DB file lifecycle

b.db owns the entire data layer — schema reconcile, audit chain, sealed columns, the works. That's the right tradeoff when the framework owns the deployment. But operators with their own schema management, their own migration tool, or a Mongo-style document model still want the framework's at-rest encryption + periodic re-flush + WAL-checkpoint snapshot logic without giving up their own connection.

b.db.fileLifecycle(opts) packages just that slice:

1. Decrypt /db.enc (or opts.encryptedDbPath) to a random tmpfs file (/dev/shm/-.db). 2. Surface the plaintext path (lifecycle.dbPath) — operators open their own new DatabaseSync(dbPath). 3. Periodically (every flushIntervalMs) re-encrypt the plaintext file back to /db.enc, after running PRAGMA wal_checkpoint(TRUNCATE) against the operator's connection so committed pages are folded in. 4. Provide snapshot(db) for backup callers — same envelope as the on-disk encPath, returned as a Buffer. 5. Provide flushAndCleanup(db, opts) for graceful shutdown — force-flush, optionally remove the plaintext sidecar.

The DB encryption key is read from / created at opts.dbKeyPath (default /db.key.enc). The key file is itself vault-sealed (operator's b.vault instance) — turning the key into per-row data still doesn't help an attacker without the vault keypair.

Composes: - b.crypto.encryptPacked / decryptPacked — same envelope b.db writes, including the deployment-bound AAD. - b.atomicFile — durable writes that don't leave a partial db.enc on a crashed flush. - operator's b.vault instance — seals the DB key on first generation and unseals it at boot.

The framework does NOT touch the SQLite handle — every method that needs to issue SQL takes the operator's db argument explicitly. This keeps the lifecycle primitive composable with any sqlite-shaped layer (node:sqlite, better-sqlite3, bun:sqlite).

b.db.fileLifecycle(opts) #

stable0.8.62
{
  {
    dataDir:           string,                   // operator's data dir (used as AAD)
    tmpDir?:           string,                   // tmpfs path; default /dev/shm on Linux
    allowDiskFallback?: boolean,                 // permit os.tmpdir() fallback (warns)
    encryptedDbPath?:  string,                   // default /db.enc
    encryptedDbName?:  string,                   // basename under dataDir (default "db.enc")
    dbKeyPath?:        string,                   // default /db.key.enc
    vault:             ,       // for sealing the DB key
    label?:            string,                   // AAD label (default "default")
    flushIntervalMs?:  number,                   // default 5 minutes
  }
}

Returns an encrypted-DB-file lifecycle handle. Methods:

- decryptToTmp() — decrypt the encrypted DB file to a fresh tmpfs path and return the path. Idempotent: subsequent calls return the existing path. - dbPath — the resolved plaintext-tmpfs path (set after decryptToTmp() runs). - startFlushTimer(db, opts?) — start a periodic flush timer against the operator's SQLite handle. Returns a stop function. - flushNow(db) — force a single re-encrypt flush (WAL checkpoint + write encPath atomically). Used by backup paths. - snapshot(db) — return the encrypted Buffer (same envelope as flushNow writes), without touching the disk encPath. - flushAndCleanup(db, opts) — shutdown sequence: flushNow, close the handle, optionally remove the plaintext file + WAL/SHM sidecars.

var lc = b.db.fileLifecycle({ dataDir: "/var/lib/app", vault: b.vault });
var dbPath = lc.decryptToTmp();
var db = new (require("node:sqlite").DatabaseSync)(dbPath);
var stop = lc.startFlushTimer(db);
// ... operator runs the app ...
process.on("exit", function () { lc.flushAndCleanup(db, { removePlaintext: true }); });

Last updated 2026-08-08T16:39:49.652Z by seeder.