Skip to content
CapsuleDB
Esc
↑↓navigate↵open⌘Jpreview

Ship persistence with your package. Use the database the app already owns.

CapsuleDB lets an Effect library own its tables, typed queries and migrations inside the host app's existing database. The host keeps its client. The package keeps its schema private.

Read the quick start
npm i capsuledb effect
export const capsule = Capsule.make({
  id: "reference.tokens",
  tables: [tokens, tokenAudit],
  migrations: [
    Migration.make({
      id: 1,
      name: "create-tokens",
      risk: "additive",
      steps: [Migration.createTable(tokens)],
    }),
    Migration.make({
      id: 2,
      name: "add-token-audit",
      risk: "additive",
      steps: [Migration.createTable(tokenAudit)],
    }),
  ],
  layer: tokenLayer,
});
psql example_appPostgreSQL

example_app=> \dt

Tables in an example host database, and who owns each
NameOwned by
accountsapp
sessionsapp
capsule_reference_2e_token_auditreference.tokens
capsule_reference_2e_tokensreference.tokens
capsuledb_registry_ledgercapsuledb
capsuledb_registry_metadatacapsuledb

example_app=> select capsule_id, migration_id, name from capsuledb_registry_ledger;

Migrations CapsuleDB's ledger recorded for the capsule
capsule_idmigration_idname
reference.tokens1create-tokens
reference.tokens2add-token-audit
Registry.statusReady

The reference capsule prefixes its tables with the namespace CapsuleDB derives from its ID. The host cannot rename it, and the tables stay out of the package's public API.

Three steps from package to host

Each block is a region of examples/quick-start.ts, which CI typechecks and runs against the built package.

  1. Declare the table

    The package declares its table once. CapsuleDB renders it for PostgreSQL and SQLite.

    const visits = Schema.table("capsule_example_2e_visits", {
      columns: {
        path: Schema.text(),
        hits: Schema.integer(),
      },
      primaryKey: ["path"],
    });
  2. Export one capsule

    Tables, an append-only migration history and an Effect service, bundled as one module constant the host can import without running anything.

    export const capsule = Capsule.make({
      id: "example.visits",
      tables: [visits],
      migrations: [
        Migration.make({
          id: 1,
          name: "create-visits",
          risk: "additive",
          steps: [Migration.createTable(visits)],
        }),
      ],
      layer: visitsLayer,
    });
  3. Compose it over the host's client

    The host lists the capsules it trusts. Registry.layer applies pending migrations before any service answers, and still requires the host'sSqlClient.

    Run the whole quick start
    export const CapsulesLive = Registry.layer({
      provider: SqliteBun.profile,
      capsules: [capsule],
    });

In use: DomainKit ships its PostgreSQL storage as one CapsuleDB capsule, @domainkit/capsuledb.

The boundary in one minute

A silent film on what the package owns, what the host owns, and how preparation reaches Ready.

Read the transcript
Capsule anatomy: the reference capsule's tables, append-only migrations and typed OneTimeTokens service
Capsule anatomy
Host composition: several capsules and the host's own persistence share one host-owned SqlClient and database
Host composition
Preparation and readiness: Registry.layer reads the ledger, applies pending migrations and reaches Ready
Preparation and readiness

One migration history, four providers

Pick the profile that matches the host's client. The logical history stays the same.

ProfileDialectExecutionNotes
Pg.profilepostgresTransactionalPreparation serializes with an advisory lock.
SqliteBun.profilesqliteTransactionalHost-owned Bun SQLite client.
Libsql.profilesqliteTransactionalHost-owned libSQL client.
D1.profilesqliteAtomic batchBounded batches. No interactive transactions or Effect steps.

Read the provider guide

Where it stops

CapsuleDB coordinates migrations and provides a capsule's service over the host's client. It does nothing past that line.

Not an ORM
No public tables, rows or query builders. Persistence details stay inside the capsule.
Not a deployment service
It does not invoke Wrangler or manage provider resources. The optional Alchemy resource prepares a database the host already runs.
Not a second connection
The host's client stays the host's, from open to close.
Not a sandbox
Namespace collisions are rejected, but trusted capsule SQL is not isolated. The host composes only what it trusts.

Guides

The full contract, from both sides of the boundary.