Deploy to staging and production

You need: a Cloudflare account, a bootstrap token, and an origin declared for the environment.

The order, and why it is three commands

Putting it on the internet is the first-time walkthrough, pithy deploy is the last of the three, and CI/CD is the same order unattended.

Provision, then migrate, then deploy. Each refuses to do the next one’s job.

CommandDoesDoes not
pithy provisionCreates account resources, writes their ids, migratesShip code
pithy migratePromotes schemaSeed, or deploy
pithy deployShips Workers, then proves what answeredMigrate. Provision

A deploy that silently created account resources would be hard to review, and these are exactly the resources worth reviewing.

The first time

pithy provision --env staging --yes
pithy secrets provision
pithy migrate --env staging
pithy deploy --env staging

That middle step is the one people miss. Provisioning cannot create the secrets sealed under a master key — only a deployed manager can, and provisioning runs before the managers exist. So it says which, by name, rather than reporting success on an environment that cannot serve a request.

Every time after

pithy migrate --env prod --json
pithy deploy --env prod --json

Two gated steps rather than one convenient one, because the failure modes and the remedies are different.

Deploy warns when the schema is behind and never runs anything.

It refuses before it builds

Three checks run with a named environment, before anything is spawned.

An environment with no origin

Every auth callback, magic-link URL and CSRF allowed-origin is derived from an environment’s origin. State none and each of them invents one — and the dangerous invention is production’s.

That is how a staging deploy emails real users magic links pointing into production.

Three shapes are refused, each naming the Worker, the environment and the edit: no origin at all, an origin nothing routes, and the public workers.dev subdomain left open beside a custom domain.

That last one is subtle. The default leaves it on and declaring a route does not change it, so the Worker answers on both — and on that other origin the base URL names the custom host, so the same-origin gate refuses exactly the requests that establish who somebody is. Turn it off, or turn it explicitly on to say you meant both.

An environment that does not bind what its Workers declare

A job declared and never synced ships with no binding and no cron trigger. The binding fails on the first request, and the cron simply never fires — nothing errors, nothing logs, nothing probes red.

One comparison of the whole table, one remedy: pithy worker sync.

A binding with no resource behind it

Refused, naming the provision command.

pithy doctor reports all three without being asked, so they are findable before a deploy is attempted.

It proves what it shipped

After a Worker ships, deploy probes that Worker’s declared domain for /health and asserts the version answering matches the version wrangler just reported.

Not the URL wrangler printed, which under gradual deployments may be a version-scoped preview. And not a liveness check, because the old version answering happily is precisely the failure worth catching.

OutcomeFails the command
verifiedNo
inconclusive — a rollout in progress, or no version idNo
mismatch — one other version answering consistentlyYes
unreachable — nothing answered at allYes

A front end builds first, with the environment

Because the build resolves each capability’s client-safe values for a named environment — and a build without it inlines dev values into a production bundle, silently.

A failed build fails that Worker and skips its deploy: shipping a Worker whose assets are stale is worse than not shipping it.

One Worker’s failure does not abort the batch

Every Worker is attempted and reported, and the command exits non-zero if any failed. There is no flag to ship just one — deploy ships the whole set.

Production takes a phrase

pithy provision --env prod --yes --confirm "yes, i really want to provision prod"

The phrase names its environment, so one typed for staging cannot be pasted into a command targeting production.

Interactively you are asked. Headlessly it must arrive by flag — so a headless production provision happens only when a person wrote the phrase into the pipeline on purpose.

The account is resolved before anything else

A pinned account that disagrees with the resolved credentials refuses before wrangler is ever spawned, naming both.

That refusal is the point: a deploy that authenticates against the wrong tenant succeeds, and says nothing.

There is no teardown for a declared environment

Deliberately. A branch’s environment is disposable and has a destroy command. Staging and production are not, and the one-word difference between the two is not a difference a flag should carry.

Delete them in Cloudflare, by hand, on purpose.

Check it worked

  • Deploy reports verified for each Worker
  • pithy env --env prod shows every binding provisioned
  • pithy doctor exits 0
  • The health route answers at your real domain with the version you just shipped
ESC