Skip to content
FLAVIO COPES
flaviocopes.com
2026

Cloudflare Workers: secrets and environments

By Flavio Copes

How to handle config, secrets, and multiple environments (staging, production) in Cloudflare Workers without leaking anything.

~~~

Every real app has two kinds of config: stuff that’s fine to see, and stuff that must stay secret. And most apps run in more than one place: production, and at least a staging copy to test on.

Cloudflare Workers handle both cleanly. Let me walk through how I set it up.

Plain variables

For config that isn’t sensitive, like a feature flag or your app’s public URL, use vars in wrangler.jsonc:

{
  "vars": {
    "APP_URL": "https://myapp.com",
    "DEBUG_LOG": "0"
  }
}

These show up on env in your Worker:

export default {
  async fetch(request, env) {
    return new Response(env.APP_URL)
  },
}

These live in your config file, in your repo. So only put things here that you’re fine with the world seeing.

Secrets

API keys, tokens, signing keys: these must never go in your repo. They’re secrets, and you set them with Wrangler:

npx wrangler secret put STRIPE_KEY

It prompts you for the value, encrypts it, and stores it on Cloudflare. It’s not written to any file. In your code, a secret looks exactly like a var:

const key = env.STRIPE_KEY

The difference is where it lives. Vars are in your config; secrets are encrypted on Cloudflare and never in your repo.

Secrets in local development

When you run wrangler dev, you still need those secrets, but you obviously don’t want the real production ones on your laptop.

Put local values in a file called .dev.vars:

STRIPE_KEY=sk_test_local_key

Wrangler loads it automatically in dev. Add .dev.vars to your .gitignore so it never gets committed.

Multiple environments

Now the environments. You define named environments in your config, each with its own settings:

{
  "name": "myapp",
  "vars": { "APP_ENV": "dev" },
  "env": {
    "staging": {
      "name": "myapp-staging",
      "vars": { "APP_ENV": "staging" }
    },
    "production": {
      "name": "myapp-prod",
      "vars": { "APP_ENV": "production" }
    }
  }
}

Deploy a specific one with --env:

npx wrangler deploy --env production

Each environment is a separate Worker with its own name, its own URL, its own secrets. Set a secret for just production:

npx wrangler secret put STRIPE_KEY --env production

So staging can use test keys and production uses live ones, with no chance of mixing them up.

The gotcha that gets everyone

Here’s the one that cost me time, so learn it from me.

Environment blocks do not inherit the top-level config. If you put triggers, vars, or bindings at the top level, your named environments do not get them automatically. You have to repeat them inside each environment.

So if you have a cron trigger at the top level and a production environment, production won’t run the cron unless you also add the trigger inside the production block.

It feels wrong the first time, but it’s deliberate: every environment is fully explicit, so nothing leaks across by accident. When you add a binding, remember to add it to each environment that needs it.

The setup I use

Three environments: a local dev one, a staging copy that mirrors production, and production. Public config in vars, anything sensitive as a secret, local values in .dev.vars.

It’s a little boilerplate to repeat bindings per environment, but the payoff is that production and staging can never accidentally share a database or a key. The full reference is in the secrets docs and the environments docs.

~~~

Related posts about cloudflare: