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.
Get your license key
A license is issued automatically the first time you complete a purchase, find it at
/dashboard/license: one offline-verifiable Ed25519 token per major
version, with a copy-token button. Verify it anytime, offline, with verifyLicense() from
@caisson/license-verify: no network call required.
Install
bunx @caisson-sh/cli@latestWith no flags, create-caisson runs interactively in a terminal (TTY): it prompts for a
licensed build vs. the free Apache-2.0 sample vs. the full-catalog demo, then your project name
and module selection. A scripted, non-interactive run needs an explicit --name plus either
--edition <bundle> (which auto-selects the bundle's current modules) or at least one
--module <id@version> (repeatable; also overrides or extends an edition's auto-selection).
Run bunx @caisson-sh/cli@latest --help for the full flag reference.
Free, no license key: --sample and --demo need no module selection and no
CAISSON_LICENSE_TOKEN. --sample installs only public-npm Apache-2.0 packages; --demo
composes the full catalog with every commercial module replaced by a local stub, and its
generated .npmrc still maps @caisson:registry to registry.caisson.sh: tokenless, since that
registry serves the open module set to an unauthenticated request.
bunx @caisson-sh/cli@latest --demo --name my-app # full catalog, commercial modules stubbed
cd my-app
bun installLicensed install: every other generated project ships a .npmrc pointing @caisson:registry
at registry.caisson.sh with the auth token read from CAISSON_LICENSE_TOKEN: export your
dashboard license key under that name before bun install.
export CAISSON_LICENSE_TOKEN=<the token from /dashboard/license>
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 registry.
The generator release. Each published @caisson-sh/cli release bundles a snapshot of the
registry index at build time, so the id/version pairs --module can resolve against are frozen to
whatever the registry looked like when that CLI version shipped.
bunx @caisson-sh/[email protected] --name my-app --module @caisson/[email protected]@latest always resolves against the newest snapshot; pin an explicit npm version (@0.4.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 @caisson-sh/cli@latest --name my-app \
--module @caisson/[email protected] \
--module @caisson/[email protected]- Each value splits on the last
@, so a scoped id (@caisson/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 one of that module's published versions exactly.^1.4.0,~1.4,1.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 registry allowlist): "@caisson/typo"
unknown version for @caisson/kernel: "9.9.9"The validation call chain
@caisson/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/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/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
edition?: BundleId; // legacy edition ids normalize to a bundle id at this boundary
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 @caisson-sh/cli@latest --name my-app --edition compliance --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), or from your dashboard. No extra wiring beyond the
CAISSON_LICENSE_TOKEN you already export for bun install.
Wire the MCP server (tool-native)
@caisson/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/mcp-serverIt is an auth-gated, entitlement-scoped 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 buyer session, the same pattern Claude Desktop /
Claude Code use for any local stdio MCP server:
import { runStdioServer } from "@caisson/mcp-server";
// Illustrative: mcpOptions (registry index + issued tokens + the onGenerate host hook) is your
// own wiring — see the McpServerOptions type exported by @caisson/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, entitlement scoping, 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/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/*packages never import one.
Caisson documentation
Compliance-grade infrastructure for regulated SaaS, the manual.
Base substrate
The sixteen Apache-2.0 packages every bundle 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, license-verify) — plus the commercial Platform tier.