npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@domainkit/capsuledb

v0.18.0

Published

DomainKit Storage on PostgreSQL, as one declarative CapsuleDB capsule

Readme

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

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

Install

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.

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 |

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.