← Back to The book shelf
#019 v11

Staging should run NODE_ENV=production

/3 min read

NODE_ENV=staging looks obviously right and is quietly wrong. The variable was never a deployment-tier label — it's a semantics switch that other people's code branches on. Set it to anything but production and the frameworks underneath you switch off the exact behaviors staging exists to rehearse.

The variable libraries read

In the weather API's env schema, NODE_ENV validates against exactly three values — development, production, test — because those are the values the ecosystem actually compares against. Express, Fastify, Nest, half of npm: they check === 'production' and change caching, error verbosity, logging, and optimization paths on the answer. Set staging and every one of those checks reads it as "not production." Staging bugs escape to production because staging silently ran different library code paths — the rehearsal happened on a stage with different scenery bolted to the floor.

The second axis

The project's answer is two variables, because there are two independent questions. NODE_ENV answers "how should code behave" and stays inside the vocabulary libraries understand. A separate APP_ENVdevelopment, staging, production — answers "which tier is this deploy." Staging runs NODE_ENV=production with APP_ENV=staging: the runtime behaves exactly as it will in production, while the deployment still knows its own name. Both variables are enum-validated at boot, so a typo dies at startup instead of living on as a silent "not production."

One variable felt like enough right up until the repo grew docker-compose.staging.yml next to docker-compose.production.yml. At that point, staging that lies to its libraries stopped being a hypothetical and became a file I could open — the second axis appeared the moment there was a second tier to be honest about.

What the tier axis buys

The tier variable does visible work. Swagger UI is mounted only when APP_ENV isn't production — interactive docs available to poke at in development and staging, hidden where the public lives. The same repo's one-image rule leans on the split too: build a single environment-agnostic image and promote the identical artifact across tiers, which only works if the tier is runtime input rather than something baked into the build. Neither decision is expressible when the only variable you have was already spent on semantics.

The Monday check is one line: look at what your staging currently exports as NODE_ENV. If it says staging, your dress rehearsal has been running with the understudy.

The mechanism here is Node-specific — Rails and Spring carved this boundary differently and earlier — and my scar tissue is all from Node, so that's where I'll bound the claim. Two axes covered this project completely. What I don't know is how many axes a real fleet needs — region, tenant, compliance regime — before per-variable axes collapse into a config service, and whether that collapse counts as the scheme failing or just growing up.

Share: