stable

Production cloud store

A Postgres-backed vault with unattended server unlock: at-env managed secret, the mandated Shamir recovery, and hot/cold placement via routeStore.

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 extended to-* store) + @noy-db/at-env (sealing) + @noy-db/on-shamir (the mandated recovery) — routeStore/wrapStore are 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. The at-* 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 to createNoydb.
  • A PostgresClient — any object with query(sql, params) → { rows }; hand it your pg pool.

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.