@domainkit/capsuledb

中文文档
0.23.0发布于 2026/11/03MITTypeScriptES Module构建来源已验证近 30 天下载 2500

DomainKit Storage on PostgreSQL, as one declarative CapsuleDB capsule

原始使用文档
文档目录6 节
  1. Install
  2. Wire it up
  3. Or emit the SQL and assert at boot
  4. Or prepare at deploy time
  5. Tables
  6. What it guarantees

@domainkit/capsuledb

DomainKit's Storage on PostgreSQL, as one declarative CapsuleDB capsule. Install it under DomainKit.layer and the whole durable lifecycle — provider authorizations, connections, attachments, interactive-flow continuations, plan/approval/receipt attempts, multi-domain batches, and observed readiness — lives in your database, scoped to your tenants.

Documentation: domain-kit.dev

The host owns the SqlClient and its lifetime. This package owns its own tables and never exposes rows, queries, or a raw client.

Install

Requires CapsuleDB >=0.6.0 <0.7.0 and Effect >=4.1.2 <5.0.0.

bun add @domainkit/capsuledb capsuledb @effect/sql-pg

Wire it up

import { Config, Layer } from "effect";
import { PgClient } from "@effect/sql-pg";
import { Cloudflare, Custody, DomainKit, Vercel } from "domainkit";
import { PgStorage } from "@domainkit/capsuledb";

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`: `domainkit/server`'s 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") })),
);

PgStorage.layer() prepares at boot: it creates CapsuleDB's ledger, applies pending migrations, and only then provides Storage. A capsule service can never observe a database whose tables are missing.

Custody.layerConfig() reads DOMAINKIT_CUSTODY_KEY and seals every provider credential before it reaches a row. Swap it for a KMS with Custody.layerFromAsync; there is no plaintext mode, and this package never sees one.

Or emit the SQL and assert at boot

If your migrations are yours to run, take the SQL instead:

capsuledb emit \
  --module ./node_modules/@domainkit/capsuledb/dist/index.mjs \
  --export capsule \
  --dialect postgres \
  --out ./drizzle

That writes CapsuleDB's ledger DDL, the capsule's migration, and a readiness row, plus a capsuledb.emit.json index naming the files it owns. Apply them with your own pipeline, then boot in assert mode:

PgStorage.layer({ mode: "assert" });

Assert mode applies nothing and fails unless the database already matches the capsule, so a missed migration is a boot failure rather than a runtime surprise. capsuledb check compares an emitted folder against the current capsule in CI.

Or prepare at deploy time

A serverless host prepares the database in its deploy and boots without touching the schema. PgStorage.manifest() yields the manifest of exactly the capsule PgStorage.layer() installs, so a stack using capsuledb/alchemy prepares what the function asserts:

import { PgStorage } from "@domainkit/capsuledb";
import * as CapsuleDB from "capsuledb/alchemy";

const manifest = yield * PgStorage.manifest();
const registry =
  yield *
  CapsuleDB.Registry("domainkit-storage", {
    url,
    provider: "Postgres",
    manifest,
  });
// Pass registry.fingerprint into the function's version-scoped environment.

The function boots with readiness: "first-use":

PgStorage.layer({ mode: "assert", readiness: "first-use" });

The layer builds without a statement; the first Storage query runs one cached check. A failed check surfaces as a SqlError on that query and is retried on the next. Pass the same prefix and registryPrefix to manifest and layer.

Tables

TableKeyHolds
domainkit_authorizationsidprovider grant, account label, capabilities, revocation state, sealed credential
domainkit_connectionsidthe principal-facing handle over one authorization
domainkit_attachmentsid, unique (owner_id, domain)domain, zone, provider target
domainkit_continuationsidinteractive-flow state with a TTL
domainkit_attemptsidplan, approval, receipt, status, lease, failure
domainkit_readiness(owner_id, domain)latest observation, per-requirement evidence, backoff
domainkit_batchesid, 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

A batch item is a pointer: the plan, approval, receipt, lease, and failure it shows a customer all live on the attempt it names, so nothing is stored twice and the batch's status is recomputed from those attempts inside the transaction that moves it.

Readiness is keyed by domain rather than by attachment, so a host observing public DNS alone gets the same row; attachment_id links the attachment when one exists and is cleared when it is removed.

Every table carries owner_id, and every query filters by the Principal.Service your host provides, so a row belonging to another tenant reads as absent. No foreign keys are declared, to your tables or between these; add the ones you want in the emitted SQL.

PgStorage.layer({ prefix }) renames the tables. The prefix is part of the physical layout: it changes the rendered DDL and the migration checksum, so fix it before the first deploy and never change it after. registryPrefix does the same for CapsuleDB's own ledger tables and must match capsuledb emit --prefix.

What it guarantees

  • Aggregate transitions — approve, claim, complete, fail, capability promotion, credential replacement — run in one transaction over a FOR UPDATE row.
  • A continuation is consumed by DELETE ... RETURNING, so a replayed OAuth callback fails NotFound instead of connecting twice.
  • Revocation is two-phase: mark pending, call the provider outside the transaction, then delete the row only while it still holds the credential that was revoked. A crash in between, or a refresh that rotated the credential mid-revoke, leaves a row recoverRevocations finishes later, so a newly issued credential is never orphaned at the provider.
  • withLock takes a session advisory lock on a reserved connection and fails Busy rather than waiting, so a credential refresh single-flights without holding a transaction across an HTTP call.

The package passes Testing.conformance.storage from domainkit/testing, the same suite the in-memory implementation passes.

版本与发布记录

@domainkit/capsuledb 共发布 25 个版本,首次发布于 2026/09/01;过去 12 个月发布了 25 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.23.0latest2026/11/0310 天前
0.22.12026/11/0112 天前
0.22.02026/10/2717 天前
0.21.02026/10/091 个月前
0.20.12026/10/051 个月前
0.20.02026/10/031 个月前

其他发布通道:temp → 0.0.0-tegami-trusted-publish-setup。

关于 @domainkit/capsuledb 的常见问题

@domainkit/capsuledb 是什么?

DomainKit Storage on PostgreSQL, as one declarative CapsuleDB capsule

如何安装 @domainkit/capsuledb?

在项目目录运行 npm install @domainkit/capsuledb,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:effect(>=4.1.2 <5.0.0)、capsuledb(>=0.6.0 <0.7.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@domainkit/capsuledb 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@domainkit/capsuledb 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@domainkit/capsuledb 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.10.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@domainkit/capsuledb 可以商用吗?

@domainkit/capsuledb 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@domainkit/capsuledb 的下载量有多少?

根据 npm 官方下载统计,@domainkit/capsuledb 在 2026/10/13 – 2026/11/11 这 30 天内被下载 2,500 次,日均约 83 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@domainkit/capsuledb 还在维护吗?

最新版本 0.23.0 发布于 2026/11/03(10 天前);至今共发布 25 个版本,其中过去 12 个月发布了 25 个;首次发布于 2026/09/01。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。