HomeProjectsBlogContact

Environment Variables Done Right: .env Files, Secrets and Config

"It works on my machine" is usually a config problem. A practical guide to .env files, public vs private variables, validation at startup, and keeping secrets out of your repo.

Environment Variables Done Right: .env Files, Secrets and Config

A surprising number of production bugs aren't code bugs at all. The code is fine; it's reading the wrong database URL, a missing API key, or a variable that silently came through as undefined. Getting environment variables right is one of those boring skills that saves you from very exciting outages.

Why config lives outside the code

The idea is simple: the same code should run in development, staging and production, and only the configuration changes. Database URLs, API keys, feature flags, third-party credentials: all of it comes from the environment, not from constants buried in your source.

That gives you three wins:

  • Security. Secrets never get committed, so they don't end up in your Git history forever.
  • Portability. Deploying to a new host means setting variables, not editing code.
  • Fewer "oops" moments. You can't accidentally point local tests at the production database if production credentials never exist on your laptop.

The .env file, used properly

A .env file is just a convenient way to load variables during local development:

DATABASE_URL=postgres://user:pass@localhost:5432/myapp
STRIPE_SECRET_KEY=sk_test_xxx
NEXT_PUBLIC_SITE_URL=http://localhost:3000

The rules that keep it safe:

  1. Add .env to .gitignore on day one, before your first commit. Not after.
  2. Commit a .env.example with every key and a fake or empty value. New teammates (and future you) can copy it and fill it in.
  3. Never use your local .env in production. Set real values in your host's dashboard (Vercel, Netlify, Render, your VPS's process manager, and so on).

Public vs private: the mistake that leaks keys

Frontend frameworks bundle some variables into the JavaScript that ships to the browser. Anything in that bundle is public. Anyone can open DevTools and read it.

Each framework marks browser-exposed variables with a prefix:

Framework Exposed to the browser
Next.js NEXT_PUBLIC_*
Vite VITE_*
Create React App REACT_APP_*
Nuxt values under runtimeConfig.public

So NEXT_PUBLIC_STRIPE_SECRET_KEY is a contradiction. If a value must stay secret, keep it on the server and call it from an API route or server function. The browser gets the result, never the key.

A good habit: when you add a prefix, say out loud "I'm fine with the whole internet reading this." If that sentence feels wrong, drop the prefix.

Validate config when the app starts

The worst config bugs are the silent ones. process.env.API_URL is undefined, your fetch goes to undefined/users, and you lose an afternoon. Fail fast instead. Check required variables at startup and crash with a clear message:

// config.js
import { z } from 'zod'

const schema = z.object({
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
})

export const config = schema.parse(process.env)

Now a missing or malformed variable stops the deploy immediately instead of breaking a random request at 2 a.m. You don't need Zod for this; a ten-line function that throws on missing keys works too. The point is that the check exists.

Bonus: import config everywhere instead of touching process.env directly. You get one place to see every setting the app depends on.

Things that trip people up

  • Variables are strings. FEATURE_ENABLED=false is the string "false", which is truthy in JavaScript. Parse booleans and numbers explicitly.
  • Changes need a restart or rebuild. Most dev servers read .env once at startup. And browser-exposed variables are baked in at build time, so changing them on your host means redeploying.
  • Quotes and special characters. Values containing #, spaces or $ may need quoting depending on the loader. If a password "doesn't work," print its length (not the value) to check it's arriving intact.
  • Different names per environment. Keep the same variable names everywhere; only the values change.

Where real secrets should live

For a solo project, your host's encrypted environment settings are perfectly fine. As things grow, look at a dedicated secret manager (your cloud provider's offering, or tools like Doppler or Infisical) so you can rotate keys and audit who accessed what.

Whatever you use, follow two rules: give each environment its own credentials, and rotate a key the moment you suspect it leaked. If that has already happened, here is exactly what to do after leaking an API key.

FAQ

Should I commit my .env file to a private repo?

No. Private repos get cloned to laptops, shared with contractors, connected to CI services and occasionally made public by accident. Commit .env.example and keep real values in your host or a secret manager.

Why is my environment variable undefined in the browser?

Either it lacks your framework's public prefix (by design, since it's server-only), or you changed it without rebuilding. Browser-exposed variables are inlined at build time.

Is it safe to put a Firebase or Supabase key in frontend code?

Their public (anon) keys are designed to be exposed, but only because access is controlled by security rules or row-level security. Those rules are what actually protect your data, so make sure they're turned on and tested.

Keep reading

More from the workbench

All articles →