The rule
Scope is per entry as well as per environment: the reference has the field, and the two backends both honor it.
Every environment your project declares is managed. No other environment is.
dev is never in the set and needs no excluding — it is local-only, resolved from the project’s own Miniflare-backed store and seeded from the dev secrets file. Core refuses to let a declaration name it at all.
The bug this replaced
This was a closed list of two names: staging and prod.
That made the type system say a custom environment could not be managed while the rest of the CLI cheerfully accepted one. pithy migrate --env live ran. <project>-live-db would have been created. And pithy secrets provision, iterating the fixed list, gave live no master key, no manager, and no store entry — silently, until the first request.
The gap was not that the list held the wrong two names. It was that a project had no way to say which environments it had, so three parts of the CLI guessed differently.
Now the project says so, once, in the root config. Everything reads that — provisioning stands one registry up per managed environment, and dev is never among them because its values live on your machine instead.
Why managed is not narrower than declared
It could have been, and deliberately is not. The cost of widening is real and worth naming:
Everything that iterates this set multiplies with it, and the largest item is a manager Worker deployed per environment — each with its own rotation cron and its own D1. A project declaring five environments gets five managers.
The tempting alternative is a second, smaller list: declared, but only these are managed, so the common project pays for two.
So the cost is charged where it belongs: declaring an environment is what costs a manager. A project that does not want five managers declares fewer environments — which is equally true of five D1 databases, five KV namespaces, and five sets of resource names.
The declaration is the one place that decision is made, and the one place pithy doctor can see it.
Scope, as a per-secret axis
Separately from which environments exist, each secret declares whether its value is the same everywhere:
| Scope | A CLI write |
|---|---|
environment | Targets one environment |
global | Fans out to all of them |
A global secret still gets one canonical value; it is bound the same way by every environment’s Worker. The manager’s own Cloudflare API token is the clearest example — one token, written once, bound identically everywhere.
And global fills the environment slot with the literal word rather than being omitted, so the naming rule has no exception to remember. The naming facade makes that a property rather than a string, which means no call site can typo the scope into a near-miss of a real environment.
What this looks like when you get it wrong
A stale environment spelling fails at the naming facade — here, rather than by naming a store entry nothing binds. The facade validates the environment once, in one place, so production is refused rather than quietly standing up a fourth environment nothing else knows about.