Cloudflare API tokens

You make one token by hand. Every other credential the project needs is minted from it, scoped to exactly what needs it.

The bootstrap token

pithy token mints and rotates these, the config directory is where one lives on your machine, and a CI pipeline is the main reason to mint a narrower one.

The one you create in the Cloudflare dashboard, and the only one you ever create there.

It lives in <config>/cloudflare.json — account-scoped, mode 0600, outside every checkout, written by pithy init at the one moment you are already holding it. Or it arrives as environment variables, which is how CI supplies it, overlaying the file per key.

At minimum it needs API Tokens → Edit, at account scope, so the CLI can create, roll and delete account-owned tokens.

The delegation rule

Cloudflare only lets a token create another token whose permissions it already holds.

So the bootstrap token must itself hold every permission it will delegate: the union of what your composed capabilities need CI to do, plus the token-editing permission above.

Permission names are resolved against your account at mint time. An unknown or missing one fails loudly, never silently mis-scoped — which is the failure worth designing against, because a token that is quietly missing one permission works until the day it does not.

ci-system — the one CI credential

CI runs migrate and deploy in one process under one credential, so there is one token for it.

pithy token mint ci-system --env prod

Its permissions are the base — deploy Workers, migrate remote D1, read and write the Secrets Store — plus whatever the composed capabilities need CI to do. You never hand-list them, and composing a capability that contributes a permission takes effect on the next mint.

Getting the value into CI

This is the one place a credential has to cross a gap by hand, and it is worth understanding why.

CI has no Pithy config directory, and neither secret store is readable from outside a Worker. So the flow is: mint the token, read its value out, and set it as your CI system’s own secret.

--store dev-vars (the default) writes the value to <config>/<project>/tokens.json, mode 0600, keyed by environment. Nothing in the CLI ever reads that file. It is a handoff to a person: you open it, copy the value for the environment you minted, and paste it into your provider’s secrets.

--store ephemeral writes nothing at all — for a job that mints and uses a token in the same step.

Nothing was ever written into the checkout, for any environment. It used to be a per-environment file in the repo, which put a live production credential in a directory npm pack can reach.

Worker-consumer tokens

A deployed Worker that needs to call Cloudflare — the secrets manager is the clearest case — reads a token through its own binding, from the Secrets Store.

Those are minted the same way, with --store secrets-store, and no human is in the loop at all.

Token names are project-scoped

A minted token is named <project>-<env>-<profile>.

Cloudflare’s token list is account-wide and flat, so the project segment is what keeps two Pithy projects in one account from listing, rotating and revoking each other’s credentials. pithy token list filters on that prefix and shows nothing outside it.

The store entry a value is written to is scoped the same way. The variable name is not — a CI variable is a key your pipeline reads rather than a name in a shared namespace.

Rotation is mint, store, then delete

Cloudflare has no single call for it, so pithy token rotate is the proven two-step: mint a new token under the same name and policies, store the value, then delete the tokens that predate it.

--keep-previous leaves the old one standing as a grace window while a Worker consumer picks up the new value. Redeploy first, then revoke.

pithy token revoke deletes every account token of the profile’s computed name for that environment — and the project segment in that name is what keeps the sweep inside your project.

No command ever prints a token value

Not on stdout, not in --json, not in an audit event, not in an error.

The value goes to the store the profile resolves, and the output says only which store, and where. That is a property to rely on rather than a habit: the payload has no field that could carry one.

Every mint, rotate and revoke is audited when the project composes audit — carrying the token’s id and its store, never its value.

The account is resolved before the credentials are read

A project says which account it belongs to in its root config. That resolution happens first, before any credential file is opened.

Minting into a file the project did not select would create a real token in somebody else’s account — which is the failure that ordering prevents. A pinned account id that disagrees with the resolved credentials refuses before anything is spawned, naming both.

ESC