← Back to list

Stop Letting Broken Env Variables Ruin Your Production Deploy

I’ve shipped that bug. You’ve shipped that bug. Let’s fix it once.

Codeaprogram · 2026-04-16 10:26 · 0 claps · 3.3 min read
#env #nextjs #validation
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Stop Letting Broken Env Variables Ruin Your Production Deploy

I’ve shipped that bug. You’ve shipped that bug. Let’s fix it once.

It’s 2 AM. Pagerduty is screaming. You SSH into the box, dig through logs, and eventually find it — DATABASE_URL was undefined the entire time. The app started fine. Health checks passed. The first hundred requests? Also fine, sort of. But the moment someone actually tried to write to the database, everything fell apart.

The .env file had a typo. One character. That’s it.

I’ve been there more times than I’d like to admit.

The Problem Nobody Talks About Enough

We spend so much energy on type safety inside our code. TypeScript, Zod, Prisma schemas — we’ve built this whole ecosystem to make sure our data is shaped correctly. And then we just… read environment variables as raw strings and hope for the best.

const port = process.env.PORT; // string | undefined. Cool, great, thanks.

You cast it to a number somewhere, probably with a parseInt, maybe with a fallback that's wrong, and then you forget about it. Until you don't.

The thing is, the environment is the most likely place for a config mistake. It’s where secrets go, where URLs differ between staging and prod, where someone copy-pasted something wrong six months ago and it’s been quietly waiting.

What envzod Actually Does

envzod is a small library that does one thing really well: it validates your environment variables against a Zod schema the moment your app starts. If something’s wrong, it throws before your server ever handles a request.

import { createEnv } from "envzod";
import { z } from "zod";

export const env = createEnv({
  DATABASE_URL: z.string().url(),
  PORT:         z.coerce.number().default(3000),
  NODE_ENV:     z.enum(["development", "test", "production"]),
  JWT_SECRET:   z.string().min(32),
});

That’s basically it. You write the schema once, and from that point on, env.PORT is a number. Not string | undefined. A number. TypeScript knows it. Your editor autocompletes it. The runtime guarantees it.

And when something is wrong, you don’t get a cryptic runtime crash deep inside your ORM. You get this:

╔════════════════════════════════════════════╗
║  envzod: Invalid Environment               ║
╚════════════════════════════════════════════╝

✗ DATABASE_URL
    Invalid url
    Got: "localhost/mydb"
✗ JWT_SECRET
    String must contain at least 32 character(s)
    Got: "tooshort"
  Fix the above and restart your server.

Clear. Human-readable. Field by field. I genuinely appreciate that it shows you what it got, not just what it expected.

The CLI Check Is the Part I Actually Love

Okay so the runtime validation is great. But the thing that really got me was the CLI.

npx envzod check --env .env.production

You can run this in CI before the deploy even kicks off. If the environment is wrong, the pipeline fails here — not after your pod spins up and starts serving broken responses to real users.

# GitHub Actions
- name: Validate environment
  run: npx envzod check --env .env.production

This is such a simple idea. Catch it at the gate, not in the ward.

Next.js Is a Special Kind of Pain

If you’ve used Next.js, you know about NEXT_PUBLIC_* variables. The short version: webpack needs to see literal process.env.NEXT_PUBLIC_FOO references at build time to inline them into client bundles. Dynamic access doesn't work. It's a hard constraint, not a quirk.

So most solutions make you repeat yourself. You define the variable in your schema, and then you also have to write process.env.NEXT_PUBLIC_FOO somewhere explicitly. envzod doesn't pretend this limitation doesn't exist — it just gives you a clean way to handle the split:

import { createNextEnv } from "envzod/next";
import { server, client } from "./envzod.config";

export const env = createNextEnv({
  server,
  client,
  runtimeEnv: {
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
    // Server keys are auto-sourced — only client keys go here
  },
});

Server variables get pulled from process.env automatically. Client variables, you wire up once. The schema itself lives in envzod.config.ts, which is also what the CLI reads. One source of truth.

How It Compares to t3-env

t3-env is the other popular option and it’s genuinely good, especially if you’re already deep in the T3 stack. But a few things about envzod stand out:

The server variable repetition. With t3-env, you have to explicitly pass every server-side env variable into a runtimeEnv object. envzod auto-sources server vars from process.env. Less boilerplate. Less chance of forgetting one.

The CLI. t3-env doesn’t have one. If you want to catch config issues before deploy, you’re rolling your own.

Outside Next.js. envzod works with plain Node, Express, Bun — anything. t3-env is more opinionated about its context.

Neither is wrong. But if you’re not tied to the T3 ecosystem, envzod feels leaner.

The Real Reason to Use This

Here’s the honest pitch: broken environment variables are embarrassing bugs. They’re the kind of thing that makes a team lose trust in its own deploy process. “Did we check the env?” becomes a pre-deploy ritual that shouldn’t need to exist.

envzod makes the env a first-class part of your app’s startup contract. Either the configuration is valid and you proceed, or it isn’t and you know exactly why before a single request is served.

That’s worth a npm install.

envzod is MIT licensed. Zod is a peer dependency, so install it separately if you haven’t already.


메타데이터
post_id
f2da462e6bea
slug
stop-letting-broken-env-variables-ruin-your-production-deploy-f2da462e6bea
url
https://medium.com/@codeaprogram/stop-letting-broken-env-variables-ruin-your-production-deploy-f2da462e6bea
canonical_url
https://medium.com/@codeaprogram/stop-letting-broken-env-variables-ruin-your-production-deploy-f2da462e6bea
author_url
https://medium.com/@codeaprogram
status
ok
fetched_at
2026-06-11 05:11:55