# Cloudflare Workers: secrets and environments

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

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-07-01 | Topics: [Cloudflare](https://flaviocopes.com/tags/cloudflare/) | Canonical: https://flaviocopes.com/cloudflare-workers-secrets-environments/

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](https://flaviocopes.com/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`:

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

These show up on `env` in your Worker:

```js
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:

```bash
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:

```js
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:

```jsonc
{
  "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`:

```bash
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:

```bash
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](https://developers.cloudflare.com/workers/configuration/secrets/) and the [environments docs](https://developers.cloudflare.com/workers/wrangler/environments/).
