A tour of the CLI
infra has eleven commands. You’ll use four of them every day, and the
rest when you need them. Here they all are, in the order you’ll meet
them, each run on a real app.
Run them from packages/infra:
pnpm infra <command> <app.ts> [flags]Every command takes the path to your app module, the file whose default
export is App.make(...). Run pnpm infra with nothing and it reminds
you:
usage: infra check|explain|secret|dev|graph|config|plan|ship|prune|destroy|state <app.ts>| Needs your account? | Changes anything? | |
|---|---|---|
graph, check, explain, config, dev |
no | no (config writes files) |
plan, state, check --account |
yes | no |
ship, secret put, prune, destroy |
yes | yes |
The ones that need your account read CLOUDFLARE_API_TOKEN (and
CLOUDFLARE_ACCOUNT_ID, if the token reaches more than one account).
Look before you leap
Section titled “Look before you leap”graph: what does my app declare?
Section titled “graph: what does my app declare?”The fastest way to see an app: its Workers, what each binds, who calls whom. No requests, nothing changes.
$ pnpm infra graph example/shop/app.tsshop-users (no handlers) serves users: get, add, names USERS_DB: d1 shop-users-dbshop-api (fetch) calls usersTwo Workers: users implements the users contract (three methods)
over a D1 database; api handles HTTP and calls it. Each name is as
it’ll be in your account: shop-users, shop-users-db. Add --json for
the same as a versioned report a script or an agent can read.
check: is anything wrong?
Section titled “check: is anything wrong?”Everything that can be caught before a deploy: types, declarations, bundle sizes. When all’s well:
$ pnpm infra check example/guestbook/app.ts✓ guestbook: no problems found (app, types, bundle)When it isn’t, each problem has a code, a place, and a fix:
error used-undeclared example/my-guestbook/guestbook.ts:43:16 the Worker uses Book, which it doesn't bind fix: bind it: .bind(…, Book)
guestbook: 1 error, 0 warnings (app, types, bundle); `infra explain <code>` says more--json gives an agent the same, --account adds what your account
says (names taken, secrets with no value, migrations pending), and
--no-types or --no-bundles skip the slower parts.
Checks and diagnostics has the whole list.
explain: what does that code mean?
Section titled “explain: what does that code mean?”$ pnpm infra explain migration-aheadmigration-ahead (error): the database has migrations the code does not
The database recorded more migrations than the code declares: newer codemigrated it, and this code is older, or another copy of the app. Deployingit would run code against a schema it doesn't know. Ship the code withthose migrations, or, if they're meant to go, undo them in a newmigration of the newer code.With no code, it lists every one, with its severity:
$ pnpm infra explainusage: infra explain <code>; the codes: used-undeclared error the code uses a resource or contract the Worker does not bind object-service-outside error a Durable Object's own service is used outside an object contract-unimplemented error a Worker calls a contract no Worker of the app implements …Build and try
Section titled “Build and try”dev: run it all, locally
Section titled “dev: run it all, locally”Every Worker of the app, in workerd, wired the way it deploys: local KV, D1, R2 and queues, service bindings between your Workers, Durable Objects, crons firing on the minute. It rebuilds when you save.
$ pnpm infra dev example/guestbook/app.tsexample/guestbook/app.ts, locally (state in .infra/dev, beside it): guestbook http://guestbook.localhost:8787/ and http://127.0.0.1:8787/ goes to guestbook ✓ applied migration 1 to D1 database guestbook-book--port picks another port. More in
Local development.
config: hand a Worker to Cloudflare’s own tools
Section titled “config: hand a Worker to Cloudflare’s own tools”infra config <app.ts> [--worker name] writes a cloudflare.config.ts
(and an entry in .infra/) for one Worker, so vite dev with
Cloudflare’s Vite plugin, or Cloudflare’s cf CLI, can run it. It only
overwrites files it wrote itself.
Go live
Section titled “Go live”permissions: what does a token need?
Section titled “permissions: what does a token need?”Before you make an API token for an app, ask what it needs. infra permissions ships the app twice and destroys it against a fake
Cloudflare, in memory, with no credentials, and says which permissions
those calls take, each with what in your app needs it:
$ pnpm infra permissions example/app.tsvisits (production): what an API token needs, measured by shipping it twice and destroying it against a fake Cloudflare
To ship it: Account · D1: Edit d1 main, infra's state Account · Workers KV Storage: Edit kv cache Account · Workers Scripts: Edit Worker api
To destroy it, besides: nothing moreThe permissions are those Cloudflare’s API documents for each call, or measured where it documents none; what neither covers is listed for you to check. A permission on a zone is for the zones of your domains and routes.
plan: what would a ship do?
Section titled “plan: what would a ship do?”Reads your account and the state, and says what would change. It changes nothing, so run it as often as you like.
$ pnpm infra plan example/guestbook/app.tsguestbook (production)+ d1 book: guestbook-book~ the D1 database guestbook-book: the next ship applies migration 1: migration-pending+ Worker guestbook-guestbook: new, live at once~ rollout: 100%+ is new, = unchanged, ~ changing. A resource you stopped declaring
shows as ~ too, “kept until prune”: a ship never deletes data on its own. --env plans another
environment, --json gives a report, and --out plan.json saves the plan
with everything it read, for ship --plan to ship exactly that.
ship: do it
Section titled “ship: do it”$ pnpm infra ship example/guestbook/app.ts 1.28s shipping guestbook-guestbook 1.29s ✓ 739.1 KB of modules 2.01s ✓ created D1 database guestbook-book 2.47s ✓ applied migration 1 to D1 database guestbook-book 3.68s ✓ created Worker guestbook-guestbook, live 8.16s ✓ smoke test passed: 3 × GET /, answered by the new Worker 8.51s ✓ shipped guestbook-guestbook: https://guestbook-guestbook.you.workers.devAll or nothing: if a step fails, or you press Ctrl-C, it puts everything back. The flags you’ll reach for:
--env pr-7 |
ship a separate copy, with its own resources |
--steps 25,100 |
the rollout’s steps (default 10, 50, 100) |
--no-smoke |
skip the smoke tests |
--atomic |
all at once, when a step-by-step rollout can’t work (adding a Durable Object class) |
--plan plan.json |
ship a saved plan, only if nothing it read has changed |
--allow-breaking |
ship a contract change that breaks callers still on the old version |
--take-routes |
take routes and domains another Worker serves |
--no-check |
skip the offline checks (not recommended) |
Plan and ship walks through the rollout.
secret put: give a secret its value
Section titled “secret put: give a secret its value”printf %s "$STRIPE_KEY" | pnpm infra secret put test/fixtures/secrets/app.ts stripe-keySets a declared secret on every deployed Worker that binds it, reading the value from standard input (typed without echo, at a terminal), so it never lands in a file or your shell history. For a Secrets Store secret, it sets it in the store, and makes the store if your account has none.
Keep it tidy
Section titled “Keep it tidy”state: what does the account remember?
Section titled “state: what does the account remember?”$ pnpm infra state example/guestbook/app.ts2026-10-17T02:18:59.624Z destroyed: worker guestbook-guestbook deleted, d1 guestbook-book deleted2026-10-17T01:47:05.339Z shipped: guestbook: 3bd046c7The resources the app owns (none, here: it was torn down), and its
history, newest first. --json for the full record.
prune: delete what you stopped declaring
Section titled “prune: delete what you stopped declaring”A resource you remove from your app isn’t deleted by the next ship: it’s
unbound, kept with its data, and marked an orphan. plan lists them.
infra prune <app.ts> <name> deletes the one you name;
--allow-data-loss deletes every orphan at once.
destroy: tear an environment down
Section titled “destroy: tear an environment down”$ pnpm infra destroy example/guestbook/app.ts --env pr-7 --yes✓ deleted worker guestbook-pr-7-guestbook✓ deleted d1 guestbook-pr-7-bookWorkers first, then their resources, buckets emptied before they’re
deleted. It always wants --yes, and production’s data is guarded:
$ pnpm infra destroy example/guestbook/app.ts --yesstopped: tearing down production deletes its data; pass --allow-data-loss if that's meantWhen two ships meet
Section titled “When two ships meet”Each app and environment has a lock, so two ships can’t run at once. The second one stops and says who has it:
stopped: guestbook (production) is being deployed by vm:8138:7i7y32, since 2026-10-17T01:48:23.012Z; try again when it's done, or after 2026-10-17T02:18:23.012Z, when a lock that deploy left is taken overA ship you stop with Ctrl-C undoes its work and lets go. One killed outright can’t, so its lock is taken over after 30 minutes. (That’s a real one: a ship killed mid-rollout while this guide was being written.)
- How it fits together: what these commands do underneath.
- Plan and ship: the rollout, step by step.