pithy add secretsNo prerequisites. This is the bottom of the dependency graph, and it is usually composed for you as a side effect of adding something else — auth, email, payments, support, storage, media and turnstile all declare it as a peer.
What lands in your repo
apps/<worker>/pithy.config.ts gains the registration with an empty registry:
secrets({
registry: {},
rotationIntervalDays: 30,
}),The registry is scaffolded empty and left for you. It is one of the options --set refuses, and deliberately: a registry entry is an object with four declarations in it, and that is not something anybody types at a prompt.
Every capability contributes its own slice at runtime, so an empty registry here does not mean an empty registry in the Worker — it means you have declared no secrets of your own yet.
apps/<worker>/wrangler.jsonc gains one binding it can write, and one it cannot:
| Binding | Type | Written by |
|---|---|---|
SECRETS | d1 | add |
SECRETS_ENCRYPTION_KEYS | secret | provision |
A secret binding has no array in wrangler.jsonc at all until an account has been reached: the entry needs a store_id and a secret_name that do not exist yet. add reports it in notes rather than writing an entry wrangler would refuse.
What runs, and what is minted
add runs the dev migrations for the secrets database, and mints your dev master key into <config>/<project>/secrets.jsonc — outside every checkout, and reaching each Worker through its generated .dev.vars.
Minted a dev master key as SECRETS_ENCRYPTION_KEYS, into ~/.config/pithy/acme/secrets.jsonc.
Local only, and it reaches each Worker's generated .dev.vars.It is written only when absent. Minting a new one would make every locally stored secret undecryptable.
It records the Secrets Store id
This is the part that matters to other commands. pithy add secrets records SECRETS_STORE_ID in the account config, and four other commands refuse without it — pithy email, pithy media, pithy storage and pithy payments all deploy a Worker that decrypts its credentials from that store.
If you see The CF Secrets Store id is missing from one of those, this command is the answer.
The two config options
| Option | Default | What it decides |
|---|---|---|
registry | {} | Your secrets, keyed by name — each declaring its backend, scope, origin and rotation. Edited by hand |
rotationIntervalDays | 30 | How often the at-rest encryption key rotates. The manager re-encrypts every stored secret on this cadence |
Then provision
pithy secrets provisionThat stands up the per-environment infrastructure, in order: the manager’s own least-privilege token, then per environment a dedicated D1, a minted master key, the migrated schema, and the deployed manager Worker. Every step is idempotent.
It also writes your secrets_store_secrets stanza — the binding add could not — and creates every secret the registry says nobody chooses: an arbitrary value like a signing key is minted, and a value that must match something issued elsewhere is left as a question for the person who can answer it.
An existing value is never replaced. Creating a missing secret and replacing a live one are different acts, and only the first happens there.
Without provisioning, a deployed Worker composing secrets comes up with no SECRETS_ENCRYPTION_KEYS and fails on its first request. That is the failure this step exists to prevent, and pithy doctor names it before a deploy does.
pithy secrets is the whole command surface — create, update, rotate, remove, list, edit, provision and deprovision.
Declaring your first secret of your own
Add an entry to registry and run pithy secrets create <name>:
registry: {
ACME_WEBHOOK_SECRET: {
backend: "d1",
scope: "environment",
origin: { kind: "obtained", issuer: "acme" },
rotation: { kind: "manual", issuer: "acme" },
},
},The four declarations are what let one command do the right thing for it forever after. Secrets works through what each one means.