Production cloud store
Audience: a team moving a vault off the laptop — a real database backend, a server that unlocks without a human, and data placed on the right storage tier. Bundle: core +
@noy-db/to-postgres(or any extendedto-*store) +@noy-db/at-env(sealing) +@noy-db/on-shamir(the mandated recovery) —routeStore/wrapStoreare core exports.
Verified by: showcases/src/recipe-cloud-store.recipe.test.ts — the code below is the same code that runs there (the real published to-postgres adapter over the same in-memory wire its conformance suite uses).
Three production concerns, one story: the backend (Postgres holds ciphertext rows and can't read one of them), the unlock (no secret typed on a server — at-* sealing, with the recovery mandate that makes it safe), and placement (the audit log on its own backend, resilience middleware around everything).
Prerequisites
- Packages:
pnpm add @noy-db/hub @noy-db/to-postgres @noy-db/at-env @noy-db/on-shamir - A sealing key in the environment —
NOYDB_SEALING_KEY, 32 random bytes base64 (openssl rand -base64 32), injected by your secret manager. Theat-*family is deliberately not zero-knowledge: the host holding this key can decrypt what it unseals — that's its purpose. shamirRecovery: shamirRecoveryProvider()— managed mode refuses a vault with no strong recovery enrolled (see below); the Shamir profile needs its provider passed tocreateNoydb.- A
PostgresClient— any object withquery(sql, params) → { rows }; hand it yourpgpool.
The backend — Postgres holds ciphertext
import { createNoydb } from '@noy-db/hub'
import { toPostgres } from '@noy-db/to-postgres'
import { Pool } from 'pg'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const store = toPostgres({ client: pool }) // autoMigrate: true — creates noydb_envelopes
One table, envelope rows, versioned writes. A DBA with full access sees id, collection, v, and an envelope column whose _data is ciphertext — the paired test asserts the dump contains none of the record content.
The unlock — managed secret, mandated recovery
A server can't type a secret. In managed mode the hub generates one, seals it under your provider's key, and persists the sealed envelope inside the vault — mutually exclusive with secret by design:
import { atEnv } from '@noy-db/at-env'
import { shamirRecoveryProvider } from '@noy-db/on-shamir'
const db = await createNoydb({
store,
user: 'server',
secretMode: 'managed',
sealingKey: atEnv(), // reads NOYDB_SEALING_KEY
shamirRecovery: shamirRecoveryProvider(), // backs the mandated recovery profile
})
And here is the part that surprises everyone the first time: managed mode refuses a vault with no strong recovery enrolled. A sealed secret nobody can recover would turn sealing-key loss into total data loss, so the first open must enroll one:
const { vault } = await db.team.openVaultAndEnrollRecovery('acme', {
recovery: [{ profile: 'shamir', k: 2, n: 3 }], // 3 shares, any 2 recover
})
Distribute the three Shamir shares to separate custodians. From then on, every process restart is just openVault('acme') — same store, same env key, no secret anywhere in your code or config. The paired test proves the round trip: boot #1 writes, boot #2 (a fresh instance) reads it back.
The placement — hot/cold routing + resilience
routeStore() is a core export — collections map to backends, and the result satisfies the same store contract as any single backend:
import { routeStore, wrapStore, withRetry, withCircuitBreaker } from '@noy-db/hub'
import { s3Bundle } from '@noy-db/to-aws-s3'
const resilient = wrapStore(
routeStore({
default: store, // Postgres — the hot path
routes: { auditLog: coldObjectStore }, // the append-heavy log on cheap storage
}),
withRetry({ maxRetries: 3 }),
withCircuitBreaker({ failureThreshold: 5 }),
)
const db = await createNoydb({ store: resilient, /* …unlock as above */ })
Placement is per collection and exact — the paired test asserts each envelope lands on exactly one backend. For aging records rather than whole collections, archive sweeps by policy; for period-based migration, periods' migratePeriod works on a cold route.
Verify
Run the paired walkthrough: ciphertext-only rows (the DBA's view is asserted), the managed-mode recovery refusal path, the sealed secret surviving a restart, and exact hot/cold placement.
See also
- Where can noy-db run? — this recipe is the "server-only behind my API" topology, made concrete.
- store capabilities · to-* catalog — picking the backend.
- routing — the full middleware set (
health-check,cache,logging,metrics). - broker — short-lived cloud credentials when the store is AWS and the client shouldn't hold keys.