Provision Secrets

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:

KindLimit
A Secrets Store entryNo documented Cloudflare cap — held to Pithy’s own ceiling
An API token labelFree text, no cap
A Worker script63, 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.

ESC