Almost everything else in the kit composes this one, and most people never think about it. That is the intended experience: secrets is the capability that means no other capability has to invent its own answer to where does the Stripe key live.
It gives your project one registry — a declaration of every secret the project needs, what kind of thing each one is, and where it lives — and two backends behind it. Capabilities declare their slice; you declare yours; the accessor resolves the whole set once per invocation and hands each capability a view over only its own names.
The registry is the definition
Nothing writes a secret the registry has never heard of. pithy secrets create SOMETHING_NEW on an undeclared name is refused before anything is sent, and that refusal is the feature: a secret that exists in a store and nowhere in your code is a secret nobody will rotate, audit or remember.
Each entry declares four things.
| Declaration | Choices | What it decides |
|---|---|---|
| Backend | d1, cf-secrets-store | Where the value lives. D1 for anything only your Worker reads; the Cloudflare Secrets Store for a value something outside the Worker needs |
| Scope | environment, global | Whether staging and production hold different values, or one value everywhere |
| Origin | minted, obtained | Whether the kit can produce the value, or a human takes it from somebody else’s console |
| Rotation | local, provider, manual | Who replaces it, and how |
Those four are what let one command do the right thing for every secret without a list of special cases. pithy secrets rotate branches on the declaration and on nothing else — never on a name.
The master key never leaves the Worker
A d1 secret is encrypted at rest under a master key that lives inside that environment’s secrets manager Worker. Nothing reads it back out — not the CLI, not a dashboard, not you.
That has a consequence worth stating plainly, because it shapes several commands: a stored value cannot be copied from one environment to another. A global secret that ended up in staging and not production cannot be completed by reading staging’s; the repair is to remove it everywhere and mint one fresh.
The key rotates on its own axis, inside the manager, on the manager’s own cron — every 30 days by default. Every stored secret is re-encrypted on that cadence. It is the one secret nothing replaces in place, because replacing it would leave everything sealed under a key nobody holds.
Your dev values are outside every checkout
Local development reads <config>/<project>/secrets.jsonc, in the Pithy config directory. Not .dev.vars, and not anything in your repository.
It is the source, not a file something copies out of. Edit a value there and the next pithy dev hands the Worker the new one, with no intermediate step; delete one and it is gone from every generated file, with no stale copy anywhere to fall back to.
So there is nothing to gitignore, nothing git add -A can reach, nothing npm pack can carry, and nothing an rm -rf on a clone destroys. Delete the whole checkout and your secrets are still there. Every worktree of one project resolves the same file with no setup step.
pithy secrets edit opens it — knowing a path outside your checkout is not the same as having a way to open it.
A value never comes from a flag
create and update read from stdin when it is piped, and from a masked prompt otherwise. A flag would leave a live credential in shell history and in every process list on the machine.
Nothing in this capability’s surface prints a value back, on any command, in either output mode. The rotation result has no field that could carry one, so it is structural rather than a habit.
What it deliberately does not do
It does not fetch from a third-party vault. No HashiCorp Vault driver, no AWS Secrets Manager. Two backends, both Cloudflare’s, both in your account.
It does not do per-request secret injection from outside. Everything is resolved at the start of an invocation and shared for its duration.
It does not rotate what it cannot reach. A manual secret prints the console, the page and the command that records the result, and calls nothing. That is honest rather than incomplete: nobody’s API returns a new App Store Connect key.
A keyspace is not a secret. An entry can declare a keyspace — a family of keyed values your application mints per tenant or per version — and those are written in-Worker through the accessor, never by a command. pithy secrets ls marks them, because a keyspace is the one entry an operator must not try to set.
When you would reach for it
You will not choose it; it will arrive. auth, email, payments, support, storage, media and turnstile all declare it as a peer capability, and createBackend refuses to assemble any of them without it.
You will think about it the first time you need a credential of your own — a webhook signing secret, a third-party API key — and want it in the same place, with the same rotation story, as the ones the kit brought with it.
What it needs
Nothing. It is the bottom of the graph.
It gets better with controlplane, which exposes reading the registry and rotating a value to a management client, and with audit, which records every set, rotation and removal — naming the secret and the environments reached, never the value.