experimentalsince 0.3.0-pre.6

Satellite collection vocabulary

Canonical terms for satellite collections: satellite, joined handle, existence authority, and pairing marker.

noy-db is an encrypted, offline-first document store; a single database instance is called a vault, and records live in collections within it. Every record is stored as a single envelope — one AES-256-GCM ciphertext blob that is decrypted whole or not at all; there is no partial decryption. Satellite collections (experimental, since 0.3.0-pre.6) split a record's small, frequently-read fields and its large, rarely-read fields into two collections — two envelopes — so hot reads never pay to decrypt the heavy fields. The terms below name the moving parts; the satellites service page covers the full mechanics.

Satellite (collection)

A collection hung off a base collection, sharing the base's record id 1:1 and holding the heavy or less-accessed fields in its own envelope. Declared with the satelliteOf option on vault.collection(); the fields list is the routing table naming exactly which fields the satellite owns.

import { createNoydb } from '@noy-db/hub'
import { toFile } from '@noy-db/to-file'

const db = await createNoydb({
  store: toFile({ dir: './data' }),
  user: 'ann',
  secret: 'a-strong-secret',
})
const vault = await db.openVault('mail')

// Base: small, hot fields — cheap to decrypt on every list.
const msgs = vault.collection('msgs')

// Satellite: heavy fields, same record ids, its own envelope per record.
vault.collection('msgs_text', {
  satelliteOf: 'msgs',
  fields: ['subject', 'body'], // routing table — the fields this satellite owns
  joined: 'msgs_full',         // optional: registers the joined handle
})

// Joined handle: the full record, split on write, merged on read.
const full = vault.joined('msgs_full')
await full.put('m1', { from: 'ann@example.com', subject: 'Hi', body: 'Long message body…' })

await msgs.get('m1') // hot read — the heavy envelope is never fetched or decrypted
// → { from: 'ann@example.com' }

await full.get('m1')
// → { subject: 'Hi', body: 'Long message body…', from: 'ann@example.com' }
  • Lineage: the zero-knowledge-store analog of PostgreSQL TOAST / vertical partitioning / Data Vault 2.0's hub-and-satellite pattern.

Joined handle

JoinedHandle<T>, obtained via vault.joined(name) once a satellite declares the joined option. It reads and writes the full record: a read merges base ⊕ satellite — a disjoint field-union computed from two O(1) same-id gets, with every declared satellite field defaulting to null when no satellite row exists yet; a write splits the record by the routing table and fans out to both collections. The handle is deliberately narrow — get / put / delete / list / describe only — and is not typed as Collection<T>: reactive members (subscribe, live, …) are simply absent from the type rather than stubbed.

Existence authority

The base row is the sole authority on whether a record exists. A satellite row is existence-filtered: satellite.get and joined get return null, and satellite list() drops the id, whenever the base row is absent or tombstoned. A tombstone is the residue a vault.forget() crypto-shred leaves behind — the envelope rewritten with its ciphertext and wrapped record key stripped, so the record reads as permanently gone. (Satellite query() cannot be existence-filtered and refuses outright with SatelliteConfigError; use list() or get() instead.) A base-less satellite envelope is dead ciphertext — unreachable through every enumerated read surface, though not physically swept in v1.

Pairing marker

The persisted { base, fieldsHash, joined } record written into the reserved _schemas collection the first time a satellite is declared. A later declaration whose base, field set, or joined name diverges from the marker is refused with a SatelliteConfigError — refusal rule R-S9 in the satellites refusal matrix — so the same logical record can never silently split differently across app versions.

See also

  • satellites (experimental) — declaration options, the three handles, the full refusal matrix, convergence rules, and v1 caveats.