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
starterpack is thesaaspack minusjobs.queue— a worker needs a real Postgres, sojobs.queuejoins at theprovision dbrung, 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 breakline 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:
- 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.)
- Runs every part's migrations, plus your own under
migrations/, idempotently. - 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=1only 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), andeject(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 underparts/by hand changes its content hash and the guard hook (local) andaudit(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.com — only if you set a CTRLAI_TOKEN repo secret. With no secret, nothing leaves the runner. |
CI runs
audit+conform, notguard.guardis the local pre-commit hook. The two cover the same boundary from different sides:guardstops a drifting commit on your machine,auditstops 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 dbfirst soDATABASE_URLpoints at your Neon, not the local pglite. Auth and the/ctrlpanel 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, ornpx ctrlai <command> --help. - Per-part docs — each installed part ships
SPEC.md(what it does) andseams.md(how to wire it) underparts/<name>/. - The rules your agent follows — the scaffolded
AGENTS.mdin your app is the in-repo, enforced version of the contract.