stable

snapshots

Point-in-time backup and restore: db.snapshot() writes an encrypted checkpoint to a separate bundle store; restoreSnapshot() restores in place.
Subpath@noy-db/hub/snapshots
FactorywithSnapshots({ store })
Layerwith-fork
ClusterF — Snapshot & Portability
LOC cost~200 (off-bundle when not opted in)

What it does

A vault is noy-db's unit of encrypted storage — one logical database made up of collections of records, each individually encrypted before it ever reaches a store (see Core 01 — Vault & collections). The snapshots service adds point-in-time backup and restore for a whole vault: db.snapshot() writes a checkpoint you can come back to later with db.restoreSnapshot(), independent of whatever store the vault normally reads and writes through day to day.

Each snapshot is a .noydb bundle — the same portable encrypted container format used for exports — written to a second, separate NoydbPodStore (any adapter implementing readBundle / writeBundle / deleteBundle / listBundles, e.g. toDrive() from @noy-db/to-drive or s3Bundle() from @noy-db/to-aws-s3). Because the bundle already holds ciphertext, the snapshot destination never sees plaintext, and restoring re-uses the vault's existing keyring (its wrapped decryption keys) — no credentials need to be re-supplied. Snapshots are whole-vault only (no per-collection snapshot), and restoring is a plain overwrite, not a merge — see Limits & gotchas.

Prerequisites

  • A bundle-capable store — withSnapshots({ store }) writes checkpoints to a NoydbPodStore (readBundle/writeBundle/listBundles), e.g. s3Bundle() from @noy-db/to-aws-s3 or toDrive() from @noy-db/to-drive. A plain record store cannot receive snapshots.

Opt in

withSnapshots({ store }) designates the bundle store snapshots are written to, and is passed as snapshotsStrategy to createNoydb:

import { createNoydb } from '@noy-db/hub'
import { withSnapshots } from '@noy-db/hub/snapshots'
import { toMemory } from '@noy-db/to-memory'
import { s3Bundle } from '@noy-db/to-aws-s3'

const db = await createNoydb({
  store: toMemory(),
  user: 'owner',
  secret: 'correct horse battery staple',
  snapshotsStrategy: withSnapshots({
    store: s3Bundle({ bucket: 'my-backups', prefix: 'noydb', region: 'us-east-1' }),
    retention: { keepLast: 10 },   // optional; default keeps every on-demand snapshot
  }),
})

await db.openVault('acct')

Any NoydbPodStore works as the snapshot destination — see S3 bundle store below for the one shipped in the extended to-* family.

Usage

Take a checkpoint

db.snapshot(vaultId, opts?) requires the vault to already be open (openVault()), and returns the new snapshot's metadata:

const snap = await db.snapshot('acct', { label: 'before-year-close', note: 'FY2026' })
// snap.version   → 'acct__snap_000001'   (pass this to restoreSnapshot)
// snap.integrity → 'verified'

opts.label is a human-readable name (shown in a snapshot chooser UI); opts.note is a freeform memo. Both are optional.

List snapshots

db.listSnapshots(vaultId) reads only the sidecar index — it never downloads snapshot bytes — and returns metadata newest-first:

const snaps = await db.listSnapshots('acct')
[
  {
    "version": "acct__snap_000001",
    "label": "before-year-close",
    "note": "FY2026",
    "exportedAt": "2026-01-15T10:03:22.000Z",
    "exportedBy": "owner",
    "size": 4821,
    "integrity": "verified"
  }
]

Restore

db.restoreSnapshot(vaultId, version) restores the vault in place and runs verifyBackupIntegrity() automatically. Collection handles obtained before the call are now stale — re-obtain them afterward:

await db.restoreSnapshot('acct', snap.version)

const invoices = db.vault('acct').collection<Invoice>('invoices')   // re-obtain after restore

Throws SnapshotNotFoundError if version doesn't exist in the snapshot store (pruned by retention, or a typo). Throws BackupCorruptedError or BackupLedgerError if verifyBackupIntegrity() detects tampering.

Automatic cadence

withSnapshots({ snapshotPolicy }) enables automatic whole-vault snapshots driven by vault writes, in addition to on-demand db.snapshot(). Automatic snapshots overwrite a single rolling key (<vaultId>__auto) and are exempt from retention — the cadence timer can never evict your labeled on-demand checkpoints, and on-demand snapshot() calls preserve the rolling slot untouched.

const db = await createNoydb({
  store,
  user: 'owner',
  secret: 'correct horse battery staple',
  snapshotsStrategy: withSnapshots({
    store: snapshotStore,
    snapshotPolicy: { mode: 'debounce', debounceMs: 60_000, minIntervalMs: 300_000 },
    retention: { keepLast: 10 },   // applies to on-demand checkpoints only
  }),
})
modeTrigger
'manual' (default)No timers — db.snapshot() only.
'debounce'Fires after debounceMs of write-idle, with a minIntervalMs floor between fires.
'interval'Fires on a fixed intervalMs timer.

onUnload (default true for non-manual modes) flushes a pending auto-snapshot on tab-hide or process exit. The auto-snapshot appears first in listSnapshots() (flagged auto: true) and restores like any other: db.restoreSnapshot(vaultId, '<vaultId>__auto'). The cadence is wired off the instance's post-write hook: a write marks its vault dirty, the scheduler fires autoSnapshot() per dirty vault, and any error (or an optimistic-concurrency conflict) during that call is logged, never thrown — the vault stays marked dirty so the next cadence tick (interval) or the next write (debounce) retries. db.close() tears the scheduler down.

S3 bundle store

@noy-db/to-aws-s3 (from the extended to-* adapter family) ships s3Bundle() — a NoydbPodStore for whole-vault .noydb blobs, distinct from the per-record toAwsS3() adapter, suitable as a snapshot destination:

import { s3Bundle } from '@noy-db/to-aws-s3'

const snapshotStore = s3Bundle({ bucket: 'my-backups', prefix: 'noydb', region: 'us-east-1' })
const db = await createNoydb({
  store,
  user: 'owner',
  secret: 'correct horse battery staple',
  snapshotsStrategy: withSnapshots({ store: snapshotStore }),
})

Key scheme is {prefix}/{vaultId}.noydb; the version token is the object's S3 ETag. OCC uses S3 conditional writes (IfMatch on the ETag) — a lost race throws PodVersionConflictError. listBundles() derives metadata from paginated ListObjectsV2 calls (no per-object GET). Requires @aws-sdk/client-s3 ≥ 3.696.

API reference

withSnapshots(opts)

Enables the snapshots service. Pass the result as snapshotsStrategy to createNoydb.

OptionTypePurpose
storeNoydbPodStoreWhere snapshot blobs and the sidecar index are written. Required.
retentionRetentionPolicyPruning for on-demand snapshots. Default: keep all. Never affects the auto-snapshot slot.
snapshotPolicySnapshotPolicyAutomatic-snapshot cadence. Default mode: 'manual' — no timers.

db.snapshot(vaultId, opts?)

Takes an on-demand checkpoint of the given vault.

optsTypePurpose
labelstringHuman-readable name, e.g. for a chooser UI.
notestringFreeform memo.

Throws a plain Error if snapshotsStrategy isn't configured, or ValidationError if the vault isn't open.

db.listSnapshots(vaultId)

Lists snapshots newest-first (auto-snapshot first, if present), from the sidecar index only.

db.restoreSnapshot(vaultId, version)

Restores the vault to a prior snapshot in place. See Restore above for errors.

SnapshotMeta

interface SnapshotMeta {
  version: string          // lookup key; pass to restoreSnapshot()
  label?: string
  note?: string
  exportedAt: string       // ISO 8601
  exportedBy: string       // the acting user (NoydbOptions.user) at snapshot time
  size: number              // bytes
  integrity: 'verified' | 'legacy-unverifiable'
  auto?: true               // present only on the rolling auto-snapshot
}

integrity is 'verified' for every snapshot the current engine produces (the bundle embeds ledger metadata that restoreSnapshot() checks); 'legacy-unverifiable' is reserved for a future import path that reads pre-existing bundles with no ledger to verify against.

RetentionPolicy

interface RetentionPolicy {
  keepLast?: number     // keep only the most recent N on-demand snapshots per vault
  maxAgeDays?: number   // delete on-demand snapshots older than N days
  prune?: boolean       // false → never delete; delegate expiry to store-level infra. Default true.
}

Applied eagerly after each db.snapshot() call. keepLast and maxAgeDays can be combined. Set prune: false to use S3 lifecycle rules or similar infra-level expiry instead.

SnapshotPolicy

interface SnapshotPolicy {
  mode?: 'manual' | 'debounce' | 'interval'   // default 'manual'
  debounceMs?: number     // idle delay before firing, mode:'debounce'. Default 30_000.
  intervalMs?: number     // fixed timer period, mode:'interval'. Default 300_000.
  minIntervalMs?: number  // hard floor between auto-snapshots, any mode. Default 0.
  onUnload?: boolean       // flush a pending auto-snapshot on tab-hide/exit. Default true for non-manual modes.
  label?: string           // label applied to auto-snapshots. Default 'auto'.
}

Errors

Error classCodeThrown when
SnapshotNotFoundErrorSNAPSHOT_NOT_FOUNDversion not in the snapshot store (pruned or invalid).
BackupCorruptedErrorBACKUP_CORRUPTEDAn envelope's hash doesn't match the ledger on restore (tamper detected).
BackupLedgerErrorBACKUP_LEDGERThe hash chain, or its head, doesn't verify on restore (tamper detected).
PodVersionConflictErrorBUNDLE_VERSION_CONFLICTA snapshot or index write lost an optimistic-concurrency race (e.g. S3 IfMatch 412).

Behavior when NOT opted in

Without snapshotsStrategy: withSnapshots({ store }), db.snapshot(), db.listSnapshots(), and db.restoreSnapshot() all reject with an Error pointing at the fix:

Snapshots require the snapshot strategy. Import `{ withSnapshots }` from
"@noy-db/hub/snapshots" and pass it to
`createNoydb({ snapshotsStrategy: withSnapshots({ store }) })`.

Limits & gotchas

  • Not a replacement for history (per-record, intra-vault point-in-time). Snapshots are external, whole-vault checkpoints.
  • Whole-vault only — there is no per-collection snapshot.
  • Restore is an overwrite, not a merge. restoreSnapshot() replaces vault state in place via vault.load(); the app is responsible for checking unsaved state before calling it.
  • Single-writer safe by design, not by lock. The on-demand snapshot counter (__snap_N) is derived from an index read that isn't cross-process-locked, so two snapshot() calls racing on the same vault can compute the same blob key and the losing write silently overwrites the winning one's bytes. Multi-writer deployments (server workers, etc.) should serialize snapshot() calls at the application layer.
  • retention never touches the auto-snapshot slot — it prunes only the on-demand snapshots pool, so the cadence timer can't evict a labeled checkpoint and a labeled checkpoint can't evict the rolling auto slot.

See also