Skip to content
DomainKit
Esc
↑↓navigate↵open⌘Jpreview
On this page

@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.

Last updated on

Was this page helpful?