Two commands that look like one
pithy add storage writes the wiring, pithy provision creates the bucket, and pithy storage is the command surface afterwards. What lands in it is the object model.
pithy add storage writes bindings. It installs the package, adds storage() to pithy.config.ts, and puts a STORAGE_BUCKET entry in wrangler.jsonc. It touches no Cloudflare account and provisions nothing.
That is deliberate. An add that reached out to an account would make adding a capability something you could not do offline, or in CI, or without credentials in hand.
pithy provision stands up what those bindings point at.
What gets created, per environment
An R2 bucket — <project>-<env>-storage. One per project and per environment, for the reasons on the object model page.
A credentials secret — storage-r2-credentials, holding the account id, the S3 key pair, a scoped token and the bucket name the object store presigns with.
A sweep Worker — the deployed Worker that reclaims pending rows past their TTL, wired to that environment’s bucket.
The phase order is the contract
Every declared environment goes through phase one before any environment enters phase two:
- Preflight. Account prerequisites, most importantly a registered
workers.devsubdomain — Cloudflare requires one to deploy a Workflow-hosting Worker. Failing here means failing before a bucket exists, rather than half way through a fan-out. - Buckets, for every environment.
- Credentials, for every environment.
- Sweep Workers, for every environment.
A secret must not name a bucket that does not exist, and a Worker must not boot before the secret it reads — a Worker that boots without its credentials fails on its first multipart abort, which is a failure that arrives days later and looks like something else entirely.
Fanning each phase across all environments, rather than completing one environment end to end before starting the next, means a failure creating prod’s bucket stops the run before staging’s Worker is deployed against a half-provisioned account.
Every step is idempotent. Find-then-create for the bucket, create-then-update for the secret, and a deploy that overwrites. Re-running the whole command is a no-op.
Every declared environment is provisioned. An environment skipped here is one your project deploys to with nothing behind it.
The one thing you supply
Tearing down
The teardown reverses the order: sweep Workers first, because they bind the bucket, then the buckets.
Your files are kept by default. Only the Workers come down unless you explicitly ask for the buckets as well — those files are your data, no deploy restores them, and the flag is the confirmation.
Deleting a bucket empties it first, and that is part of the contract rather than a nicety: R2 refuses to delete a bucket still holding an object, or holding the parts of a multipart upload that was never completed. An implementation that only called the control-plane delete would work perfectly on an untouched bucket and fail on every bucket anybody had used.
Naming
The bucket is named as an R2 bucket through core’s naming facade, which is the one namespace whose rule is genuinely 3–63 lowercase characters, starting and ending alphanumeric.
The sweep Worker is named as a Worker script — 63 characters, and refused rather than truncated, because a script cannot be renamed once a deploy or a service binding points at it.