@domainkit/capsuledb
DomainKit's Storage on PostgreSQL as one declarative CapsuleDB capsule, with its options, its eight tables, and the three ways to run its migrations.
import { PgStorage } from "@domainkit/capsuledb";
| Export | Carries |
|---|---|
PgStorage |
layer(options), the Storage.Service implementation on the host’s SqlClient; manifest(options), its deploy manifest |
capsule |
The declaration itself, which capsuledb emit reads to write SQL |
VERSION |
The installed package version, read from the package manifest |
Options
| Option | Default | Effect |
|---|---|---|
prefix |
domainkit |
The table prefix. Part of the physical layout; fixed at the first deploy |
mode |
prepare |
prepare migrates at boot; assert expects the host applied the SQL |
registryPrefix |
capsuledb |
CapsuleDB’s own ledger prefix; must match capsuledb emit --prefix |
readiness |
boot |
boot checks while the layer builds; first-use checks on the first query |
PgStorage.manifest(options) takes prefix and registryPrefix and returns an Effect of the
CapsuleDB manifest for the capsule layer installs, without touching a database. A deploy stack
passes it to CapsuleDB.Registry from capsuledb/alchemy; see
Prepare at deploy time.
/**
* `PgStorage.layer()` prepares at boot: it creates CapsuleDB's ledger, applies pending migrations,
* and only then provides `Storage`. Nothing can observe a database whose tables are missing.
*/
export const DomainKitLive = DomainKit.layer({
providers: [
Cloudflare.provider({
oauth: {
clientId: Config.String("CF_CLIENT_ID"),
clientSecret: Config.Redacted("CF_CLIENT_SECRET"),
},
}),
Vercel.provider(),
],
}).pipe(
// `provideMerge`, not `provide`: the route handlers read attempts and receipts straight from
// Storage, so the layer they are given has to still carry it.
Layer.provideMerge(Layer.mergeAll(PgStorage.layer(), Custody.layerConfig())),
Layer.provide(PgClient.layerConfig({ url: Config.Redacted("DATABASE_URL") })),
);The layer requires only SqlClient. Credentials arrive sealed, so Storage never needs Custody and
never handles plaintext.
Tables
| Table | Key | Holds |
|---|---|---|
domainkit_authorizations |
id |
Provider grant, account label, capabilities, revocation state, sealed credential |
domainkit_connections |
id |
The principal-facing handle over one authorization |
domainkit_attachments |
id, unique (owner_id, domain) |
Domain, zone, provider target |
domainkit_continuations |
id |
Interactive-flow state with a TTL |
domainkit_attempts |
id |
Plan, approval, receipt, status, lease, failure |
domainkit_readiness |
(owner_id, domain) |
Latest observation, per-requirement evidence, backoff |
domainkit_batches |
id, unique (owner_id, idempotency_key) |
Batch status, approval digest, rejection |
domainkit_batch_items |
(batch_id, attachment_id) |
One domain’s place in a batch: position, attempt pointer, plan failure |
Every table carries owner_id and every query filters by the request’s Principal, so a row
belonging to another tenant reads as absent. The one exception is
Storage.continuations.header, which reads a flow’s owner, actor, provider, and expiry with no
principal and never selects payload, because the provider callback has to learn whose flow it is
finishing before it can name a principal to scope by. The package declares no foreign keys, to host tables
or between its own.
Emitting the SQL
capsuledb emit \
--module ./node_modules/@domainkit/capsuledb/dist/index.mjs \
--export capsule \
--dialect postgres \
--out ./drizzle
That writes the ledger DDL, the capsule’s migration, a readiness row, and a capsuledb.emit.json
index naming the files it owns. Apply them with your own pipeline, then boot in assert mode.
/**
* When migrations are yours to run, apply the `capsuledb emit` output with your own pipeline and
* boot in assert mode: it changes no schema and fails unless the database already matches.
*/
export const Asserted = PgStorage.layer({ mode: "assert" });capsuledb check compares an emitted folder against the current capsule in CI.
The migration is id 1, name create-storage, under capsule id domainkit.storage.