Skip to content

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:

Terminal window
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).

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.ts
shop-users (no handlers)
serves users: get, add, names
USERS_DB: d1 shop-users-db
shop-api (fetch)
calls users

Two 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.

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.

$ pnpm infra explain migration-ahead
migration-ahead (error): the database has migrations the code does not
The database recorded more migrations than the code declares: newer code
migrated it, and this code is older, or another copy of the app. Deploying
it would run code against a schema it doesn't know. Ship the code with
those migrations, or, if they're meant to go, undo them in a new
migration of the newer code.

With no code, it lists every one, with its severity:

$ pnpm infra explain
usage: 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
…

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.ts
example/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.

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.ts
visits (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 more

The 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.

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.ts
guestbook (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.

$ 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.dev

All 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.

Terminal window
printf %s "$STRIPE_KEY" | pnpm infra secret put test/fixtures/secrets/app.ts stripe-key

Sets 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.

$ pnpm infra state example/guestbook/app.ts
2026-10-17T02:18:59.624Z destroyed: worker guestbook-guestbook deleted, d1 guestbook-book deleted
2026-10-17T01:47:05.339Z shipped: guestbook: 3bd046c7

The resources the app owns (none, here: it was torn down), and its history, newest first. --json for the full record.

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.

$ pnpm infra destroy example/guestbook/app.ts --env pr-7 --yes
✓ deleted worker guestbook-pr-7-guestbook
✓ deleted d1 guestbook-pr-7-book

Workers 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 --yes
stopped: tearing down production deletes its data; pass --allow-data-loss if that's meant

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 over

A 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.)