Docs

Quickstart

Zero to a running, locked backend in one command — then climb the ladder one rung at a time, every rung landing on your accounts. This is the get-running-fast path. For the mental model behind it, read How Ctrl AI works first or after; this doc assumes nothing.

Every command, flag, and path below is the real one. If a line here ever stops matching the code, the code is right and this doc is a bug.


What you need

Requirement Why
Node 22+ The scaffold and CI both target Node 22.
git So the scaffold can install the pre-commit boundary hook and make the first commit. Optional (--no-git skips it), but recommended.
Nothing else No Docker, no database, no account, no signup. The first rung runs entirely on your laptop.

One command to a running backend

npx create-ctrl-app my-app

That scaffolds a fresh Next.js app on a verified, locked backend, then runs npm install for you. The default is the starter pack — nine verified parts that all run on a zero-config local database:

Capability What it gives you Default vendor
auth.session sign-up / sign-in / sessions self-hosted (Better Auth)
auth.tenancy organizations self-hosted
billing.subscription paid plans stripe
email.transactional transactional email resend
webhooks.ingest verified inbound webhooks stripe
storage.upload file uploads (per adapter)
ratelimit.api rate limiting self-hosted
audit.log an audit trail self-hosted
admin.crud an internal back office self-hosted

Background jobs join later. The starter pack is the saas pack minus jobs.queue — a worker needs a real Postgres, so jobs.queue joins at the provision db rung, not on the local pglite database.

Want a different shape? Pass one of:

npx create-ctrl-app my-app --pack ai-api          # starter | saas | ai-api | marketplace | backoffice
npx create-ctrl-app my-app --capabilities auth.session,billing.subscription:stripe
npx create-ctrl-app my-app --spec ./ctrl.spec.json   # a spec exported from the configurator

--capabilities overrides --pack; --spec overrides both. Other flags: --registry <source> (a URL or a local directory), --allow-community (accept community-tier adapters), --no-git, --no-install.

When it finishes you'll see the next steps it prints:

✔ scaffolded a Next.js app shell + the zero-config dev database
✔ installed the part boundary (lock, guard hook, CI)
✔ vendored 9 verified parts · pack: starter
✔ wired the seams (routes + database executor)
✔ scaffolded the /ctrl control panel
✔ wrote .env.local (local database prewired)
✔ initialised git + first commit

→ installing dependencies (npm install — the slow part)…
✔ dependencies installed

🔒 my-app is ready — a running backend your agent can't break.

Next:
  cd my-app
  npm run dev

  ▸ http://localhost:3000        your app — sign up works, end to end
  ▸ http://localhost:3000/ctrl   the control panel: parts verified, built to spec

The 🔒 … your agent can't break line is the CLI's own phrasing. Read it as the precise claim it stands for: infra changes can't land silently — they show up as a red diff and a failed check. It is not a claim that an adversary with write access cannot alter a part. See the two promises.


Run it

cd my-app
npm run dev

npm run dev runs node scripts/ctrl-dev.mjs, a small harness that, in one process:

  1. Boots a zero-config local Postgres — pglite over a socket. No Docker, no account. (If you've already provisioned a real database, it uses that instead — see the next rung.)
  2. Runs every part's migrations, plus your own under migrations/, idempotently.
  3. Starts next dev.
• ctrl: local database ready on 127.0.0.1:54329 (pglite — no Docker, no account)
✓ ctrl: applied N migration(s) (parts + your migrations/)
  ▲ Next.js 15.3.0
  - Local:   http://localhost:3000

(N is the real count printed for your pack — every part's migrations plus your own; the exact number depends on which parts you installed.)

Now open the two URLs.

http://localhost:3000 — your app

A deliberately plain shell with working sign-up / sign-in, backed by the local database. Create an account; you land on a protected /app page gated by a real session. These pages (app/page.tsx, app/app/) are placeholders — they're living documentation of how to call the seams, and they're yours to replace with your product.

The auth UI calls the part's own wired route at /api/auth/* and imports no vendor SDK — that's the point. Your app touches a part only through its seam.

http://localhost:3000/ctrl — the control panel

Every Ctrl app ships a local /ctrl route: the control panel for your backend. It shows which parts are verified, that the boundary is intact, whether the build is built to spec, and what each part costs to run.

It also has the editable vault — swap a vendor, add a capability, customize (eject), or remove — each running the real operation, then showing you the git diff and re-locking.

The panel's write actions are local-dev only. They rewrite your repo and shell out to git, so they are gated to direct loopback requests: a deployed app, or a request arriving through a proxy/tunnel, gets a 403. (Set CTRLAI_PANEL_ALLOW_REMOTE=1 only if you knowingly run dev on a remote box.) Reading the panel still works anywhere.


Add a capability

You don't hand-write infrastructure here; you vendor a verified part. To add, say, full-text search:

npx ctrlai add search.fulltext
✔ search.fulltext@1.1.0 vendored into parts/

migrations: 1 part(s) own tables — run `ctrlai migrate` after add (ledger: _part_migrations)

Now write the seams (the only code you touch):
  - search.fulltext: parts/search.fulltext/seams.md (sufficient alone)

A bare ctrlai add search.fulltext resolves the latest version (1.1.0 today); pin one with @ if you need it. This part owns a table but mounts no HTTP route, so its seam line points straight at seams.md and is sufficient alone. A part that does mount a route prints its mount instead — e.g. adding webhooks.ingest yields - webhooks.ingest: mount POST /api/webhooks/ingest (one-line re-export of webhookHandler); details: parts/webhooks.ingest/seams.md. When a part pulls new npm dependencies, an extra npm: + <pkg>@<version> line appears under the vendored line; when it needs vendor keys, an env: line lists them.

add resolves the install order, pulls anything the part requires, skips what's already installed, and scaffolds an .env.example entry for any vendor keys. You can pin a version or pick an adapter inline: ctrlai add email.transactional:postmark or ctrlai add billing.subscription@1.2.0. Run a whole pack the same way: ctrlai add saas.

After adding, install the new dependency (if any) and re-run npm run dev — the new migration applies automatically. Check your work:

npx ctrlai doctor    # is the wired app actually working? routes, migrations, env, DB seam
npx ctrlai conform   # did you build what you configured? (built to spec)

add, upgrade (version/vendor swap), and eject (take ownership) are the only sanctioned ways to change a part. They move the lock and the spec together, so the build stays green. Editing anything under parts/ by hand changes its content hash and the guard hook (local) and audit (CI) go red.


Provision a real Postgres

The local pglite database is perfect for development but ephemeral. To make your backend persistent — on your account, never ours:

export NEON_API_KEY=...           # create one at console.neon.tech/app/settings/api-keys
npx ctrlai provision db
✔ provisioned a neon database — my-app (ep-cool-name-123.us-east-2.aws.neon.tech)
✔ wired DATABASE_URL + AUTH_DATABASE_URL in .env.local (on YOUR account, never ours)
✔ ran N migration(s) on it

→ your backend is now persistent — `npm run dev` uses it automatically.
  Next rung: `ctrlai push` — put it on GitHub and turn on CI (the lock becomes a law).

Neon is the default (--provider neon); the API key is read locally and used only to create a database on your account. provision db creates the project, writes DATABASE_URL + AUTH_DATABASE_URL into .env.local, and runs the migrations. After this, npm run dev detects the non-loopback DATABASE_URL and uses your real database instead of pglite — no flag, no config change.

Other ways to read provision: ctrlai provision <part> prints the guided vendor setup for a part — each human step (e.g. the exact webhook URL or redirect URI) with the {ENV} placeholders already substituted to concrete values, plus which env vars it verifies. Run it after a vendor swap, since on-spec is not the same as working.


Push to GitHub — turn the lock into a law

export GITHUB_TOKEN=...    # a classic token with `repo` + `workflow` scopes
npx ctrlai push
✔ created you/my-app on your GitHub (private)
✔ pushed main — CI is now live: every push & PR runs `ctrlai audit` + `ctrlai conform`
  ▸ https://github.com/you/my-app
The lock is now a law: a build that touches a part fails in CI, not just at your local commit.

Next rung: `ctrlai deploy` — a live URL on your Vercel.

push creates a repo on your GitHub (private by default; --public for public), sets a clean remote with no token on disk, and pushes via a one-shot tokenized URL. The scaffold already shipped a CI workflow (.github/workflows/ctrlai.yml) — so from here, every push and PR runs:

CI step Command What it checks
boundary + attestations + wiring ctrlai audit Every part still matches its content hash; attestations and wiring are intact.
built to spec ctrlai conform The build matches ctrl.spec.json — right capabilities, right vendors, not hand-rolled.
track across your team (optional) ctrlai dashboard --push Mirrors status to app.ctrlai.comonly if you set a CTRLAI_TOKEN repo secret. With no secret, nothing leaves the runner.

CI runs audit + conform, not guard. guard is the local pre-commit hook. The two cover the same boundary from different sides: guard stops a drifting commit on your machine, audit stops it from merging.

What "the lock is a law" means precisely. CI makes infra drift a hard, red failure that blocks the merge — strong protection against the common case (a helpful agent edits a part mid-task). It is not yet cryptographic tamper-proofing: today's attestations are dev:unsigned, and every trust artifact (parts.lock, ATTESTATION.json, ctrl.spec.json) lives in the repo, so a determined writer who also rewrites the lock and the attestation can re-bless an edited part. Closing that needs a root of trust outside the repo — real signing and server-side hash enforcement — which is the cross-repo / team direction, not this free in-repo loop.

Also: conform's hand-roll detection is import-based — it catches a part being bypassed by a vendor-SDK import. A raw-HTTP reimplementation is a known blind spot we're widening.


Deploy to Vercel

export VERCEL_TOKEN=...    # create one at vercel.com/account/tokens
npx ctrlai deploy
→ deploying to Vercel (your account)…
✔ set 5 env var(s) on my-app: DATABASE_URL, AUTH_DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL, …
✔ deployment created · status: BUILDING

  ▸ https://my-app-xxxx.vercel.app
  ▸ build logs: https://vercel.com/you/my-app/deployments/...

  (still building — re-run with --wait to block until it's live, or watch the logs above)

deploy creates or selects the project on your Vercel, sets the environment (your provisioned database URL, the auth secret, vendor keys), and ships a production build. Pass --wait to block until the build finishes, --name to set the project name.

Deploy needs a real (non-local) database — run ctrlai provision db first so DATABASE_URL points at your Neon, not the local pglite. Auth and the /ctrl panel work the same on production — except the panel's write actions are disabled there (read-only in production, by design).


The ladder, end to end

npx create-ctrl-app my-app   # a running, locked backend on a zero-config DB (pglite) — boots in seconds
  ↓
ctrlai add <capability>      # vendor a verified part in (or `add <pack>` for a whole kit)
ctrlai doctor / conform      # is it wired and reachable? did you build what you configured?
  ↓
ctrlai provision db          # a real Postgres on YOUR Neon, migrated, wired
ctrlai push                  # your repo on YOUR GitHub — CI runs `audit` + `conform` on every push & PR
ctrlai deploy                # a live URL on YOUR Vercel, DB connected
Rung Lands on Needs
create-ctrl-app your laptop nothing
add / doctor / conform your repo nothing
provision db your Neon NEON_API_KEY
push your GitHub GITHUB_TOKEN (repo + workflow)
deploy your Vercel VERCEL_TOKEN

Ctrl AI never hosts your backend. The whole in-repo loop — parts, lock, panel, add/doctor/conform, the local database — is free. The cross-repo layer (a team dashboard across all your repos, private registries, server-enforced integrity, governance) is the paid tier.


Where to go next

  • The mental model — capabilities, parts, adapters, seams, the lock, the spec-loop → How Ctrl AI works.
  • The CLI reference — every command and flag is also discoverable from the tool itself: npx ctrlai --help, or npx ctrlai <command> --help.
  • Per-part docs — each installed part ships SPEC.md (what it does) and seams.md (how to wire it) under parts/<name>/.
  • The rules your agent follows — the scaffolded AGENTS.md in your app is the in-repo, enforced version of the contract.