stable

Overview

Where a record lives: the Noydb → Vault → Collection<T> hierarchy, the session lifecycle, and how the three handles map to their pages.

Always-on. Cannot be opted out. Source of truth: packages/hub/src/kernel/{noydb,vault,collection}.ts

What it is

noy-db is an encrypted, offline-first document store: your application holds the keys, and the storage backend only ever sees ciphertext. Before you read or write anything, you need to know where a record lives — that is what this page defines. Everything in noy-db hangs off a three-layer data model, implemented in the kernel (the always-on core of @noy-db/hub that every install ships, as opposed to the opt-in services):

Noydb              ← top-level instance from createNoydb()
  └── Vault        ← tenant / company namespace ("acme", "firm-2026")
        └── Collection<T>   ← typed record set ("invoices", "clients")
  • Noydb is one unlocked session. It holds the encryption keys in memory while you work and hands them back to nobody.
  • Vault is an isolated namespace — one tenant, one company, one user. Each vault has its own keyring, so access to one vault says nothing about another.
  • Collection<T> is a typed set of records inside a vault. The TypeScript generic carries through get / put / query / scan, so records come back typed.

Every record is addressed by a (vault, collection, id) triple.

Lifecycle

The full round trip — create a session, open a vault, write and read a record, close:

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

type Invoice = { amount: number; status: string }

// Without `store`, noy-db uses its built-in in-memory store — nothing
// persists past the session. Pass `store: …` to persist (see Stores below).
const db = await createNoydb({ user: 'alice', secret: 'a-strong-secret' })
const vault = await db.openVault('acme')
const invoices = vault.collection<Invoice>('invoices')

await invoices.put('inv-1', { amount: 100, status: 'paid' })
const inv = await invoices.get('inv-1')
// → { amount: 100, status: 'paid' }

db.close()  // wipes the in-memory encryption keys (KEK + per-collection DEKs)
{ "amount": 100, "status": "paid" }

get returns the decrypted record, or null if the id does not exist. The KEK (key-encryption key, derived from your secret) and the per-collection DEKs (data-encryption keys) never leave memory — Enclave covers the key hierarchy.

Handles

Each handle has its own page with its main methods:

Reserved collection names

noy-db keeps its own bookkeeping in internal collections whose names start with _:

  • _keyring — wrapped DEKs per user
  • _history — version snapshots (with withHistory())
  • _ledger / _ledger_deltas — audit ledger (with withHistory())
  • _dict_<name> — shared dictionaries (with withI18n())
  • _consent_audit — consent log (with withConsent())
  • _periods — period anchors (with withPeriods())
  • _idx/<field>/<id> — persisted index side-cars (with withIndexing())
  • _blob_* — blob slot metadata + chunks (with withBlobs())
  • _presence_<collection> — presence storage-poll fallback (with withSync())

Reserved names cannot be opened through the generic handle: vault.collection() throws ReservedCollectionNameError for _dict_*, _sequences, _links_*, and secret-bearing internals such as _sync_credentials — use their dedicated APIs instead (vault.dictionary(), vault.sequence(), vault.link()). Plaintext exports skip all underscore-prefixed collections, so internal bookkeeping never leaks into an export; vault.dump() includes them, because a full backup needs them.

Edge cases & limits

  • Memory-first: an eager collection (the default) decrypts its full record set into memory on first access. The target scale is 1K–50K records per collection; beyond that, open heavy collections in lazy mode (prefetch: false plus a cache bound) so reads go through a size-bounded LRU cache — see lazy, and indexing for querying lazy collections.
  • Concurrent writers: every stored envelope carries a version counter _v, and the store contract supports compare-and-swap on it — a write that loses a version race fails with ConflictError. Multi-record transaction() batches pre-flight these version checks and commit all-or-nothing.
  • Heavy fields: a record is one monolithic encrypted envelope — reading any field decrypts the whole record. For a collection mixing small hot fields with a large rarely-read one, satellite collections (experimental) offer an off-row modeling option.

See also