Using Secrets

Declaring one

What this capability is for is the overview; rotating one is the operation you will run most.

Nothing writes a name the registry has never heard of, so the entry comes first — every field is in the reference:

registry: {
  ACME_WEBHOOK_SECRET: {
    backend: "d1",
    scope: "environment",
    valueType: "text",
    origin: { kind: "obtained", issuer: "acme" },
    rotation: { kind: "manual", issuer: "acme" },
  },
},

Those declarations are what let one command do the right thing for it forever after. Rotation branches on the declaration and on nothing else — never on a name.

The four axes

backend — d1 for anything only your Worker reads; the Cloudflare store when something outside the Worker needs it.

It is a storage decision, not a read-time one. Moving a secret between the two is a one-line registry edit and every read site stays byte-identical.

scope — environment for a different value per environment; global for one value everywhere.

origin — whether the kit can produce the value. Set the dev value to random when it is arbitrary: any string works because nothing outside the project has to agree with it. Leave it off when the value must match something issued elsewhere.

rotation — local, provider or manual. And rotateEveryDays is separate, and worth setting on the ones nothing can rotate: a third-party key no automation will ever roll is exactly the one whose drift nothing else surfaces.

Reading one

Through the shared per-invocation accessor, not the low-level reader.

const key = await secrets.get("ACME_WEBHOOK_SECRET");

Several capabilities each read secrets within one request. Resolving independently means a round-trip per call site, and a repeated one wherever a secret is shared.

So the accessor resolves the combined registry — every capability’s slice merged — exactly once per invocation, caches it briefly, and hands each call site a precisely typed view over only its own slice.

A capability can read what it declared and cannot reach another’s names.

Versions

const { current, versions } = await secrets.getVersions("ACME_WEBHOOK_SECRET");

Every stored value is an envelope carrying its current version and every still-valid one — which is what lets a webhook verifier accept a signature from either side of a rotation while the old one drains.

Typed values

A JSON secret carries a schema, and it is validated on read. So a credential with two halves comes back as an object rather than a string you parse:

const { clientId, clientSecret } = await secrets.get("auth-google-credentials");

Both halves travel together because the pair is atomic — the client id never usefully splits off into config.

Keyspaces

An entry can declare a keyspace rather than a name: one schema, an unbounded set of members whose keys exist only at runtime. One credential per tenant, say.

Those are written in-Worker by the application that mints them, through the accessor you already hold — never by a command. pithy secrets ls marks them, because a keyspace is the one entry an operator must not try to set.

The name is bound into the ciphertext

Not encrypted — bound, as authenticated data.

A ciphertext lifted from one row and written into another does not open, because the name the decryptor supplies no longer matches the one sealed with it. That includes a keyspace member, whose bound name is the whole entry-and-key pair — so one tenant’s credential does not open under another tenant’s key.

Which secret a caller gets stops being a property of the query alone. A bug up there becomes a clean cryptographic failure rather than a disclosure.

Nothing renames a secret, which is what makes that safe: a rename is a write under the new name and a delete of the old, re-sealing through the plaintext. Editing the name column in raw SQL leaves a row nothing can open — that is the property working rather than a bug.

Setting a value

printf '%s' "$THE_VALUE" | pithy secrets create ACME_WEBHOOK_SECRET --env prod

From stdin when piped, from a masked prompt otherwise. Never from a flag — that would leave a live credential in shell history and in every process list on the machine.

Nothing prints a value back, on any command, in either output mode.

Locally

<config>/<project>/secrets.jsonc is the source, not a copy. Edit a value and the next pithy dev hands the Worker the new one, with no intermediate command.

pithy secrets edit

Opens it, validates what comes back, writes it atomically. It prints a path and a count, never a name and never a value — listing names is a different command.

ESC