Skip to content

Environment variables and secrets in production, without the ceremony

Environment variables hold the config that changes between environments, and production means they live in platform settings instead of .env files. How the local habit fails, which keys count as burned, and how to prove the plumbing before real secrets arrive.

A wall-mounted key cabinet open to show two keys on hooks and an empty spare hook, one key bow in vermilion.

By Sam Moreton

First-hand + researched

Environment variables exist to carry one kind of thing: configuration that changes between environments. The database URL, the logging level, the payment provider's key, the port: the code is identical everywhere, and each environment supplies its own values. Production means those values live in the hosting platform's settings, per environment, set through a panel, while the .env file stays a local development convenience. Two disciplines carry most of the weight here. Treat any key that touched a chat transcript, a screenshot or a near-miss commit as public and rotate it before launch. And treat anything the browser can read as published to the world.

What env vars are for

The pattern is old and boring, which is the recommendation. One codebase, many environments, differences expressed as variables the process reads at boot. It gives you promote-the-same-build deploys, because the artifact that ran in staging is the artifact that runs in production and only the variables change. It gives you rotation that is one panel edit plus a restart instead of a code change. And it draws a line that stays useful under pressure: code is shared by default, the variables are the private part.

The sorting test is easy to apply. If a value would change when the same build moves from laptop to staging to production, it is a variable: connection strings, credentials, feature flags, the base URL. If it would stay the same everywhere, it belongs in the code. Feature flags sit on the boundary and still belong on the variable side, because the whole point of a flag is that someone flips it per environment without a rebuild.

How the .env habit fails

Every failure mode comes from the file being too convenient. It sits in the project root, one directory away from git add .. It gets copied to a teammate, pasted into a chat to debug an error, screenshotted for a ticket. It travels to staging with production keys still in it, because staging was cloned from the working environment and nobody edited line three.

The repository needs a .gitignore entry for .env on day one, and a .env.example with names and no values, so the shape of the configuration is versioned while the values never are. If the file has already been committed, deleting it from the working tree fixes nothing. It lives in the history, the history is visible to everyone with read access, and the cure is rotation, which is the next section.

Which keys count as burned

Be generous with this definition, because the alternative is being generous with your production data. A key is burned if it was pasted into a chat with a coding assistant, if it appeared in a screenshot, if it sat in a commit that was pushed anywhere including a private repository, or if it was typed into a docs file or a support ticket. Coding-agent workflows burn keys as a matter of course: the transcript needs the key to debug the code, so the key enters the transcript, and transcripts are stored outside your control.

Rotation is a smaller job than the dread it causes. Create the new key at the provider, set it in the platform's environment settings, deploy, confirm the app works, then revoke the old one. The order matters: revoking first takes your own production down. Do all this before the first real deploy and it is a chore; after the first real incident it is a story.

Canary values before real secrets

Before production keys arrive anywhere, prove the plumbing with a value that does not matter. Set DEPLOY_CANARY to something obvious in the platform settings, make the app expose it somewhere safe, deploy, and look. If the canary appears, variables are reaching the process. If it does not, you have found a misnamed slot, a restart that never happened or a panel editing the wrong environment, and you found it with a string of text instead of a live credential.

The same test catches the sneakier failure. Run a full deploy with the canary in place and read the build log end to end. Any occurrence of the value in that log is a finding, and it applies to every secret that would have followed it.

What never goes client-side

Frameworks that build for the browser expose variables only when you opt in, and the opt-in is the trap. The NEXT_PUBLIC_, VITE_ and REACT_APP_ prefixes inline the value into the JavaScript bundle at build time, and the bundle is served to every visitor. A key carrying one of those prefixes is public the moment the site is. That is occasionally fine, for a publishable analytics identifier; it is never fine for anything with write permissions or a secret inside it.

The check is mechanical and worth running before every launch: build the app, then search the built output for anything resembling a key. Whatever you find in the bundle, everyone has.

Where this sits in the launch

Variables are one step of a wider checklist, and the checklist is ordered. It starts at putting an AI-built app into production, and the variables step sits between running the production build locally and pointing the app at a real database.