Two backends

The two

Both hold named entries; only one of them also holds a keyspace.

BackendWhere the value physically lives
d1An encrypted row in this environment’s secrets D1
cf-secrets-storeA native Cloudflare Secrets Store entry, bound into the Worker

The registry declares which, per secret — one registry per environment, and the reference has the shape. Nothing at a call site chooses, and nothing in the dev secrets file names a destination — the seeder derives it from the registry, so the two can never disagree.

What each is good at

d1 is the kit’s own encrypted store. Values are sealed with AES-256-GCM under the environment’s master key, and the row’s name is bound as authenticated data. It supports at-rest key rotation, keyspaces, and the rotation ledger — everything the capability offers.

cf-secrets-store is Cloudflare’s own. The value is a store entry bound into the Worker under a binding name; the platform holds it and the Worker reads it directly. Nothing in the kit encrypts or decrypts it.

The one that provisioning cannot fully wire

pithy add deliberately does not write the secrets_store_secrets stanza for a cf-secrets-store secret. That entry needs a store_id and a secret_name that do not exist until an account has been reached — and add reaches no account.

Provisioning writes it, once a Secrets Store id is in hand.

A declared secret whose entry has not been created is reported rather than bound. Wrangler refuses a config naming an absent entry, so binding it would turn one missing value into a failed deploy of the whole Worker.

Scope is a separate axis

ScopeMeans
environmentThe value differs per environment
globalOne value, identical everywhere

This drives whether a CLI write targets one environment or fans out to all of them, which is the whole practical difference. A global secret written once is written everywhere; an environment secret written in staging leaves prod alone.

rotatable is a third

It declares whether a value-rotator may manage this secret, and therefore whether it may carry multiple still-valid versions.

Storage and the read API are identical either way. It is metadata about what is allowed, not about how the value is kept.

What arbitrary and supplied mean at provisioning time

A registry entry also declares whether its value is arbitrary — a signing key, an ingest secret: any random string works, because nothing outside the project has to agree with it.

Provisioning mints those and binds them in the same pass. It stops for a supplied secret — an OAuth client secret, a payment rail’s key — because a random string there authenticates against nothing.

Absence is checked first, always. An existing value is never replaced, and replacing one is rotation, which is a separate and deliberate act.

No minted value is printed, logged, or put in an audit event.

The name is the join key

A registry entry’s name — <capability>-<what>, kebab — is the same string the dev secrets file uses, the same string a keyspace member composes from, and the same string the encryption binds as authenticated data.

One name, no mapping table. A mapping table between a file’s keys and a registry’s names would be one more thing to rot.

/ is illegal in an entry name, which is what keeps a keyspace member from ever colliding with a declared secret.

ESC