stable

Noydb

The Noydb object reference: one unlocked session that opens vaults, hooks the write queue, runs policy gates, and manages the session lifecycle.

A Noydb is the object createNoydb() returns — one unlocked session, conventionally bound to db. It holds the encryption keys in memory while you work and hands them back to nobody, and it sits at the top of the Noydb → Vault → Collection<T> hierarchy: it opens vaults, owns the keyring, and runs the client-side crypto.

Every method on this page is on that instance and works when you pass zero strategies to createNoydb() — the strategy-free kernel surface. Keyring and secret primitives live on Secret & keyring, the session-auth ladder on Authentication and Auth recovery; anything that needs an opt-in strategy is listed in the Strategies directory.

The reference is organized into groups — opening and reaching vaults, events and the write queue, session lifecycle, and policy and gates — each opening with a short note on what unites its methods, then the individual methods. Two groups that lean on session infrastructure (multi-tab coordination, translator audit) sit at the end, marked for deeper coverage in a later pass.

Opening and reaching vaults

These methods turn vault names into live Vault handles and let a session discover or fan out across the vaults it can reach. Reach for them first — everything else you do with db happens through a vault you opened here.

openVault()

openVault(
  name: string,
  opts?: {
    locale?: string
    create?: boolean
    meta?: VaultMeta
  }
): Promise<Vault>

Open a vault by name, creating it on first use, and cache it. The returned handle is a Vault. Concurrent opens of the same name are coalesced onto one Vault instance — without that convergence two callers racing past the cache miss would each build a vault with independent DEKs for the same store slice, and a record written through one would fail decryption through the other with a spurious TamperedError.

  • name — the vault name; created on first use (see opts.create).
  • opts.locale — default locale for i18n / dictKey field resolution, so you don't pass { locale } on every read. Passing it against an already-open vault re-sets the cached vault's locale.
  • opts.create — whether to self-provision on open. Default true: opening a genuinely-new vault (no _keyring/* yet) mints an owner keyring for the calling user; set false to open existing-only, which throws NoAccessError when the caller has no keyring for the vault. Either way, opening a vault already held by other principals throws NoAccessError — noy-db never self-provisions into someone else's vault.
  • opts.meta — VaultMeta (label, description); applied first-wins on the first open and ignored afterwards.

vault()

vault(name: string): Vault

Synchronous access to a vault. Call openVault() first; if the vault isn't cached this auto-opens it. Use it when you've already opened the vault and want the handle without await.

  • name — the vault to return; auto-opens it if not already cached.

listAccessibleVaults()

listAccessibleVaults(
  options?: {
    minRole?: Role
  }
): Promise<AccessibleVault[]>

Enumerate every vault the calling principal can actually unwrap, optionally filtered by a minimum role. The walk is two steps: ask the store for the universe of vaults it holds, then for each one attempt to load the caller's keyring with the in-memory secret — vaults where no keyring exists (NoAccessError) or the secret doesn't unwrap (InvalidKeyError) are silently dropped, so the return value never confirms the existence of a vault the caller cannot open. That is the existence-leak boundary: the store sees the enumeration (it owns the storage), but downstream consumers of this method see only the filtered list.

Requires the store's optional listVaults() capability; against a store without it (e.g. DynamoDB, S3, browser-local, browser-idb) it throws StoreCapabilityError, and you should pass an explicit candidate list to queryAcross() or keep a vault index out of band. Cost is O(vaults × keyring-load), so cache the result for large universes and refresh on grant/revoke. One honest edge case: a vault whose keyring has an empty wrapped-DEK map (access granted before any collection existed) passes the load probe under any secret, so an unrelated principal who knows the user-id and vault name can surface as having access to that empty vault — a metadata leak (vault name + user-id), never a content leak, since their DEK set is empty.

  • options.minRole — include only vaults where the caller holds at least this role (ordering client < viewer < operator < admin < owner); defaults to client, i.e. every vault the caller can unwrap.

queryAcross()

queryAcross<T>(
  vaultIds: string[],
  fn: (vault: Vault) => Promise<T>,
  options?: {
    concurrency?: number
  }
): Promise<QueryAcrossResult<T>[]>

Run a per-vault callback against a list of vaults and collect the results — pure orchestration, no new crypto, sync, or authorization. A per-vault failure never aborts the fan-out, so one tenant's outage can't hide the others.

  • vaultIds — the vaults to fan out across; each is opened via the normal openVault() path (honoring any cache primed by listAccessibleVaults).
  • fn — the per-vault callback, run against the resulting Vault; its return value or the error it throws is captured into that vault's result slot.
  • options.concurrency — how many vaults to process in parallel; default 1 (sequential). Bump it to 4–8 for cloud-backed stores where parallelism is the point, but beware exhausting adapter connection limits.

Events and the write queue

This group is how you observe and hook a session's write activity: subscribe to typed session events, watch the in-flight write queue, run before/after hooks, or reconcile a cross-tab write conflict. Reach for it when the UI needs to react to writes — a save indicator, an unload guard, or a conflict prompt.

The strategy-free session emits these core events over NoydbEventMap:

  • change — ChangeEvent ({ vault, collection, id, action: 'put' | 'delete' }). Fires on a completed record write in a vault.
  • error — Error. A session-level error signal.
  • write:conflict — WriteConflict. Fires when another tab overwrote a document this tab had written from an older base (the same event onWriteConflict() wraps); delivered decrypted.
  • schema:fence-changed — { vault, currentSchemaVersion, fenceState: 'normal' | 'draining' | 'migrating' | 'complete' }. A same-instance signal that a vault's schema-fence state changed, for UI integration; cross-client coordination goes through the store, not this event.

Opt-in subsystems extend the map with their own families — sync:* (with sync targets), history:* (with history), index:*, derivation:* / lookup:*, and i18n:script-violation — each documented on its own service page; they don't fire on the strategy-free surface.

How they interact. A record write is tracked on the writeQueue: writeQueue.depth increments as the write begins (so pending becomes true) and decrements when it settles, on success or failure. Around the write, onBeforeWrite hooks run first — awaited in registration order, and a throw aborts the write so nothing commits; after the write commits, onAfterWrite hooks run — awaited in order, with a handler error warned but never rolled back. Writes issued from inside a hook are suppressed by a re-entrancy guard so a writing handler can't loop. A change event signals a completed put / delete. onWriteConflict (write:conflict) sits outside this single-write sequence: it is the same-device cross-tab reconciliation signal, not a stage of one write's before/commit/after path.

on()

on<K extends keyof NoydbEventMap>(
  event: K,
  handler: (data: NoydbEventMap[K]) => void
): void

Subscribe to session events over the typed NoydbEventMap.

  • event — a key of NoydbEventMap (see the core-event catalog above).
  • handler — called with that event's typed payload each time the event fires.

off()

off<K extends keyof NoydbEventMap>(
  event: K,
  handler: (data: NoydbEventMap[K]) => void
): void

Unsubscribe a handler previously registered with on() for the same event.

  • event — the event key the handler was registered for.
  • handler — the exact function reference passed to on(); identity-matched, so pass the same reference you subscribed with.

writeQueue

get writeQueue(): WriteQueue

An observable write-queue reflecting outstanding in-flight writes across every collection. The canonical use is guarding an unload:

window.addEventListener('beforeunload', (e) => {
  if (db.writeQueue.pending) { e.preventDefault(); e.returnValue = '' }
})

onBeforeWrite()

onBeforeWrite(handler: WriteHook): Unsubscribe

Register a pre-write hook. Returns an unsubscribe function.

  • handler — a WriteHook, awaited before each write; a throw aborts the write.

onAfterWrite()

onAfterWrite(handler: WriteHook): Unsubscribe

Register a post-write hook. Returns an unsubscribe function.

  • handler — a WriteHook, awaited after each committed write; a handler error is only warned — never rolled back.

Both hooks receive a WriteEvent — { op, vault, collection, docId, before, after, baseVersion, version, userId, timestamp, txId }, with before/after delivered decrypted (before is null on a create, after on a delete). From 0.6.0-pre.4 it is exported by name from both the root barrel and @noy-db/hub/cargo, so a named handler no longer has to derive it structurally:

import type { WriteEvent } from '@noy-db/hub'

function audit(event: WriteEvent) { /* … */ }
db.onAfterWrite(audit)

Before that release /cargo published WriteHook but not the event it carries, so the only ways to name it were Parameters<WriteHook>[0] or a hub-internal import path.

onWriteConflict()

onWriteConflict(fn: (c: WriteConflict) => void): Unsubscribe

Subscribe to same-device cross-tab write conflicts (another tab overwrote a document this tab wrote, having diverged from an older base). Returns an unsubscribe.

  • fn — called with each WriteConflict, delivered decrypted so the handler can reconcile in plaintext.

Session lifecycle

These two methods suspend or end the unlocked session. Reach for lockVault to build a lock screen that scrubs a vault's keys but keeps the instance re-unlockable, and close to tear the whole session down.

lockVault()

lockVault(vault: string): void

Soft-lock a single vault: clear its in-memory keyring, DEKs, vault instance, sync engine, policy enforcer, and active-tier entry without destroying the Noydb instance. Built for a "lock screen" — DEKs are scrubbed immediately, but the same instance can be re-unlocked (via a tier-2 authenticator or tier-3 PIN) without re-running createNoydb. Quick-unlock state and the cached _meta/policy document are deliberately preserved.

  • vault — the vault to soft-lock; a no-op when it isn't currently cached.

close()

close(): void

End the session: clear the KEK, all DEKs, and the vault caches, and stop sync engines and timers. The in-process translation cache and the translator audit log are cleared here too.

Policy and gates

A policy is a vault's rulebook of named gates — one rule per sensitive operation (rotating the secret, exporting plaintext, enrolling an authenticator, granting a user) declaring the minimum session tier and any extra factor proofs a caller must present before that operation is allowed to run. Every vault has a policy from the moment it is created; if you pass none, noy-db persists a sensible default (PERSONAL_POLICY), while STRICT_POLICY is the hardened preset for regulated deployments and shared workstations. The policy is per-vault, persisted once at _meta/policy, and the same gate-check enforcement backs the session, tiers, and team services. The methods below read and replace that policy and let you pre-flight a gate yourself; see policy and session-tiers for the full gate DSL.

getPolicy()

getPolicy(vault: string): Promise<VaultPolicy>

Read the active policy for a vault — loaded from the _meta/policy document on first call, then served from an in-memory cache. Throws ValidationError if the vault has not been opened.

  • vault — the vault whose active policy to read; must already be opened.

updatePolicy()

updatePolicy(
  vault: string,
  override: Partial<VaultPolicy>
): Promise<VaultPolicy>

Replace the persisted _meta/policy document and refresh the cache. Because changing a policy is itself a privilege-management action, it is gated by the enroll-user policy.

  • vault — the vault whose _meta/policy document to replace.
  • override — a Partial<VaultPolicy> that becomes the new persisted policy.

checkGate()

checkGate(
  vault: string,
  gate: GateName,
  factors?: FactorProofBundle
): Promise<void>

Evaluate a policy gate against the active session tier and the presented factor proofs. Resolves with void on success; throws PolicyDeniedError on denial.

  • vault — the vault whose policy applies.
  • gate — the gate to evaluate: a built-in name (e.g. 'rotate-secret') or an app-defined app:* gate.
  • factors — the caller-supplied factor proofs presented for the gate.

Public envelope

getCover / setCover — the owner-curated, deliberately-plaintext vault descriptor — are documented on the cover service page, alongside the cover config opt-in.

Multi-tab coordination

Deferred for deeper coverage — multi-tab coordination is session infrastructure that will move out of the kernel reference in a future pass.

Browser-only; a graceful no-op (role 'unknown') when Web Locks / BroadcastChannel are unavailable and nothing is injected.

enableTabCoordination()

enableTabCoordination(
  opts?: TabCoordinationOptions
): { dispose: () => void }

Enable same-device multi-tab coordination — primary / secondary election plus presence. Idempotent; returns a disposer that turns it back off.

  • opts — TabCoordinationOptions tuning election / presence behavior.

tabRole

get tabRole(): TabRole

Read this tab's current coordination role.

activeTabs()

activeTabs(): TabPresence[]

List the presently-active tabs on this device.

onTabRoleChange()

onTabRoleChange(fn: (r: TabRole) => void): Unsubscribe

Subscribe to changes of this tab's role. Returns an unsubscribe.

  • fn — called with this tab's new role on every change.

onActiveTabsChange()

onActiveTabsChange(
  fn: (t: TabPresence[]) => void
): Unsubscribe

Subscribe to changes in the active-tab presence list. Returns an unsubscribe.

  • fn — called with the updated active-tab presence list on every change.

Translator audit

Deferred for deeper coverage — the translator audit log is plaintext-bypass instrumentation that will move out of the kernel reference in a future pass.

translatorAuditLog()

translatorAuditLog(): readonly TranslatorAuditEntry[]

Return a snapshot of every translator invocation since the last close() — useful for testing and compliance auditing. The log is in-memory only and is cleared on db.close(), and entries deliberately omit content hashes. It works with no strategy: with no plaintextTranslator configured (see Configuration) there are simply no entries. Configuring that translator is the one path by which plaintext can leave the zero-knowledge boundary, so this log exists to record when it did.