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.
| Command | Does | Does not |
|---|---|---|
pithy provision | Creates account resources, writes their ids, migrates | Ship code |
pithy migrate | Promotes schema | Seed, or deploy |
pithy deploy | Ships Workers, then proves what answered | Migrate. 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 stagingThat 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 --jsonTwo 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.
| Outcome | Fails the command |
|---|---|
| verified | No |
| inconclusive — a rollout in progress, or no version id | No |
| mismatch — one other version answering consistently | Yes |
| unreachable — nothing answered at all | Yes |
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 prodshows every binding provisionedpithy doctorexits 0- The health route answers at your real domain with the version you just shipped