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-caissonWith 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 installAPI 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>). versionis not a semver range, it must equal that module's catalog version exactly.^0.10.1,~0.10,0.x, andlatestare all rejected.- The same module id twice in one selection is rejected before generation runs (an ambiguous
"which version wins" case,
package.jsonand 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";| Export | Signature | Behavior |
|---|---|---|
parseArgs | (argv: readonly string[]) => unknown | Turns 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) => Selection | Zod .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-serverIt 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 checkNext steps
- Read the base substrate docs for
auth,tenancy-rls,billing, andcredits. - Read the Compliance docs to turn on WORM evidence and the audit chain.
- Bring your own framework, the
@caisson-sh/*packages never import one.
Caisson documentation
Compliance-grade infrastructure for regulated SaaS, the manual.
Base substrate
The fifteen packages every module family sits on — kernel, auth, tenancy RLS, UI tokens, billing, jobs, email, AI config, the MCP server, registry-schema, observability, rate-limit, ds-manifest, and the generator tooling (cli, migrate) — plus three modules that extend the base without joining a family.