Getting started

Install the base, wire a tenant, and run the standards gate.

Requirements

  • Bun (the runtime and package manager, never npm or yarn).
  • Postgres (Neon over HTTP is the reference host; the data layer is swappable).
  • A Postgres database you can run migrations against.

Install

bunx --package @caisson-sh/cli@latest create-caisson

With no flags, create-caisson runs interactively in a terminal (TTY): it prompts for your project name and module selection. A scripted, non-interactive run needs an explicit --name plus at least one --module <id@version> (repeatable). Run bunx --package @caisson-sh/cli@latest create-caisson --help for the full flag reference.

Every module installs from the public npm registry, so the generated project needs no token and no registry configuration:

bunx --package @caisson-sh/cli@latest create-caisson --name my-app --module @caisson-sh/kernel@<version>
cd my-app
bun install

API reference

Pin an exact install

A create-caisson run carries two independent pins: which release of the generator you run, and which exact version of each module it resolves. Neither accepts a range, every version string is matched byte-for-byte against the module catalog.

The generator release. Each published @caisson-sh/cli release bundles a module catalog built from the packages released alongside it, one version per module, so the id/version pairs --module can resolve against are frozen to the package versions of that CLI release.

bunx --package @caisson-sh/cli@0.9.0 create-caisson --name my-app --module @caisson-sh/kernel@0.10.1

@latest always resolves against the newest snapshot; pin an explicit npm version (@0.9.0) to freeze the catalog a script runs against. CAISSON_REGISTRY_INDEX=<path> points the generator at a different index file entirely, a CI/local-dev override, not part of a normal install.

--module <id@version>. Repeat the flag once per package:

bunx --package @caisson-sh/cli@latest create-caisson --name my-app \
  --module @caisson-sh/kernel@0.10.1 \
  --module @caisson-sh/auth@0.4.6
  • Each value splits on the last @, so a scoped id (@caisson-sh/kernel) keeps its leading @; a value with no @ after position 0 fails immediately (--module expects <id@version>).
  • version is not a semver range, it must equal that module's catalog version exactly. ^0.10.1, ~0.10, 0.x, and latest are all rejected.
  • The same module id twice in one selection is rejected before generation runs (an ambiguous "which version wins" case, package.json and the generated README would otherwise disagree).

Both checks run before any path is built or any file touches disk, so a bad id or version never reaches the filesystem:

unknown module id (not in the module catalog): "@caisson-sh/typo"
unknown version for @caisson-sh/kernel: "9.9.9"

The validation call chain

@caisson-sh/cli's package export (. in package.json, resolving to src/index.ts in a Bun workspace or dist/index.js published) surfaces the same pipeline the create-caisson binary runs, for a caller that wants to validate or generate without shelling out, like the MCP server's generate tool.

import {
  type Selection,
  type RawSelection,
  type GeneratedFileSet,
  DEPLOY_TARGETS,
  parseArgs,
  validateSelection,
  generate,
  runCli,
} from "@caisson-sh/cli";
ExportSignatureBehavior
parseArgs(argv: readonly string[]) => unknownTurns argv into a RawSelection-shaped object; throws on an unknown flag or a malformed --module value. Never touches the registry.
validateSelection(index: RegistryIndex, raw: unknown) => SelectionZod .strict()-parses raw, then checks every module id and version against index. Throws on the first unknown id or version, before generation starts.
generate(index: RegistryIndex, raw: unknown, engine?: GeneratorEngine) => { selection: Selection; files: GeneratedFileSet }Runs validateSelection, then hands the result to engine (templatesEngine by default). Returns the file set in memory, no disk write.
runCli(argv: readonly string[], deps: { index: RegistryIndex }) => { selection: Selection; files: GeneratedFileSet }parseArgs + generate in one call, the shape a programmatic caller uses.

RegistryIndex is loaded from @caisson-sh/registry-schema's loadRegistryIndexFromFile, reading whichever path the pinning rules above resolve to. Selection (also exported as SelectionSchema) is the validated shape validateSelection, generate, and runCli all return:

type Selection = {
  projectName: string; // /^[a-z0-9][a-z0-9-]*$/, 1-64 chars — becomes the output directory
  modules: { id: string; version: string }[]; // at least 1, no duplicate ids
  deployTarget?: "railway" | "fly" | "vercel"; // DEPLOY_TARGETS
  framework?: "next";
};

Fail-closed in the generation pipeline: generate and runCli never produce a Selection with an unresolved id or version, because both call validateSelection first and it throws rather than returning a partial result. (Parsing raw input directly via the exported SelectionSchema skips that registry check, it only enforces shape.)

Point your AI agent at Caisson

Two ways a coding agent (Claude Code, Cursor, or anything that can run a terminal command) can drive Caisson, pick the one that matches how much wiring you want to do.

Shell out to the CLI (zero setup)

create-caisson is a plain CLI, an agent that can run a terminal command invokes it exactly like you would by hand:

bunx --package @caisson-sh/cli@latest create-caisson --name my-app --module <id@version>

Repeat --module per module. --help prints the flag reference, not the live catalog, an agent (or you) gets the current registry ids and exact versions from the MCP server's list_modules/ describe_module tools (below). No extra wiring: installs come from public npm.

Wire the MCP server (tool-native)

@caisson-sh/mcp-server is a published Apache-2.0 package, it is not a scaffold default, so add it to your generated project first:

bun add @caisson-sh/mcp-server

It is an auth-gated MCP server exposing list_modules, describe_module, and generate as tools, so an agent calls them natively instead of shelling out. Host it locally over stdio with runStdioServer: one process per client session, the same pattern Claude Desktop / Claude Code use for any local stdio MCP server:

import { runStdioServer } from "@caisson-sh/mcp-server";

// Illustrative: mcpOptions (registry index + issued tokens + the onGenerate host hook) is your
// own wiring — see the McpServerOptions type exported by @caisson-sh/mcp-server for the exact shape.
await runStdioServer({
  mcp: mcpOptions,
  bearer: process.env.CAISSON_MCP_TOKEN!,
});

Then point your agent's .mcp.json at that process:

{
  "mcpServers": {
    "caisson": {
      "command": "bun",
      "args": ["run", "./mcp-server.ts"],
      "env": { "CAISSON_MCP_TOKEN": "${CAISSON_MCP_TOKEN}" },
    },
  },
}

The full tool catalog, the Bearer authentication, and the network-reachable HTTP transport (runHttpServer) for a hosted deployment are in MCP server.

Wire a tenant

Every data path goes through withTenant: the sole entry point that sets the RLS tenant context. There is no way to query tenant data without it; that is the point.

import { withTenant } from "@caisson-sh/tenancy-rls";

await withTenant(tenantId, async (db) => {
  // Inside this scope, RLS is enforced. Outside it, queries fail closed.
  return db.query.invoices.findMany();
});

Run the gate

One standards gate builds, lints, and tests every package. A package ships only through it.

bun run check

Next steps

  • Read the base substrate docs for auth, tenancy-rls, billing, and credits.
  • Read the Compliance docs to turn on WORM evidence and the audit chain.
  • Bring your own framework, the @caisson-sh/* packages never import one.