What gets stood up
pithy add secrets writes the wiring; this creates what the wiring points at, and pithy secrets is the command surface afterwards.
Per declared environment:
- A master key, as a Secrets Store entry, bound into the Worker as
SECRETS_ENCRYPTION_KEYS. - A secrets D1, migrated — the encrypted rows and the rotation ledger.
- A manager Worker, with its own rotation cron.
And once for the project:
- A scoped Cloudflare API token the managers run with, held as a Secrets Store entry and bound as
CLOUDFLARE_API_TOKEN.
Every name carries the project, and each for its own reason
The master key entry — <project>-<env>-secrets-encryption-keys.
A Cloudflare account has one Secrets Store, flat and unpartitionable, so the entry name is the only partition there is. Without the project segment, two Pithy projects in one account both resolve to the same entry: the second pithy secrets provision finds the first’s key already there, adopts it, and encrypts its rows under another project’s key. Either project’s teardown then orphans both.
The manager’s token entry — <project>-global-secrets-manager-cf-api-token.
The token is global — one value, bound the same way by every manager of this project — so the literal global fills the environment slot rather than being omitted. The naming rule has no exception to remember, and the facade makes global the same interface an environment gets, so no call site can typo the scope into a near-miss of a real environment.
The token’s own name in the account — <project>-global-secrets-manager.
Distinct from the store entry that holds its value: this is the token’s identity in the account’s API-token list, and it is the key that idempotent re-mint and teardown match on.
Three kinds of name, three budgets
Every name is composed through the naming facade, asking for its own kind of resource:
| Kind | Limit |
|---|---|
| A Secrets Store entry | No documented Cloudflare cap — held to Pithy’s own ceiling |
| An API token label | Free text, no cap |
| A Worker script | 63, refused rather than truncated |
Asking for the right kind is why the master-key entry reads …-secrets-encryption-keys rather than the hashed …-secrets-encryp-91c2e9 a 63-character budget once produced. A store entry was being held to R2’s limit for no reason at all.
The facade also validates the environment, so a stale spelling fails here rather than naming an entry nothing binds.
The binding name is fixed; only the entry is scoped
The Worker binds the master key under SECRETS_ENCRYPTION_KEYS in every project and every environment. Scoping happens in the store, not in the binding — a per-project binding name would put the project into every piece of Worker code that reads a secret.
What a re-run does
Every step is idempotent: find-then-create for resources, create-then-update for the entry, a re-mint that matches on the token name, and a deploy that overwrites.
An existing value is never replaced. Provisioning mints only what it may mint — a secret the registry declares as arbitrary, where any random string works because nothing outside the project has to agree with it. It stops for a supplied secret, because a random string there authenticates against nothing.
No minted value is printed, logged, or put in an audit event.
The declaration is what costs a manager
One manager Worker per declared environment, each with its own cron and its own D1. That is the price of declaring an environment, and it is charged in the one place the decision is made.