Environment variables are the standard way to keep secrets out of your codebase, but “just use a `.env` file” is where most tutorials stop. In practice, sloppy `.env` handling is one of the most common ways credentials leak in 2026 — through git history, logs, or client-side bundles. Here’s how to do it properly.
The basics, done right
- One `.env` file per environment: `.env.development`, `.env.staging`, `.env.production` — never share production secrets into a dev file.
- `.env` is always in `.gitignore`, with no exceptions, ever.
- Commit a `.env.example` with every variable name and a placeholder or dummy value, so new developers know what’s required.
- Never log `process.env` or any object that might contain it, even for debugging.
A clean `.env.example`
# .env.example — copy to .env and fill in real values
NODE_ENV=development
PORT=3000
DATABASE_URL=postgres://user:password@localhost:5432/app_dev
REDIS_URL=redis://localhost:6379
JWT_SECRET=replace_with_a_long_random_string
STRIPE_SECRET_KEY=sk_test_replace_me
# Never prefix server-only secrets with a public-exposed prefix
# like NEXT_PUBLIC_ or VITE_ — those get bundled into client JS
Generating strong secrets
Never hand-type a “secret” like `mysecret123`. Generate real entropy:
# 32 random bytes as hex (good for JWT secrets, session keys)
openssl rand -hex 32
# Or with Node.js, no dependencies needed
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Or a URL-safe base64 secret
openssl rand -base64 32
Client vs server: the mistake that actually leaks data
Frameworks like Vite, Next.js, and Create React App expose any env variable prefixed with `VITE_`, `NEXT_PUBLIC_`, or `REACT_APP_` directly in the browser bundle. That’s fine for a public API base URL, but it is never safe for a secret key, database password, or private token — those must stay unprefixed and server-only.
# .env — safe: server-only, never bundled
DATABASE_URL=postgres://...
STRIPE_SECRET_KEY=sk_live_...
# .env — DANGEROUS if you actually need this secret,
# because VITE_ / NEXT_PUBLIC_ vars ship to every visitor's browser
VITE_STRIPE_SECRET_KEY=sk_live_... # never do this
Loading env vars safely in code
// Node.js with dotenv, validated at startup instead of trusting blindly
import 'dotenv/config';
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
const databaseUrl = requireEnv('DATABASE_URL');
const jwtSecret = requireEnv('JWT_SECRET');
Production checklist
- Secrets in production live in your host’s secret manager (Vercel/Netlify env vars, AWS Secrets Manager, Doppler, etc.), not a plain `.env` file sitting on disk.
- Rotate secrets on a schedule and immediately after any team member offboards.
- Different secrets per environment — a leaked staging key should never unlock production.
- Restrict file permissions on any `.env` file that does live on a server: `chmod 600 .env`.
Pairing solid `.env` hygiene with a proper `.gitignore` closes most of the common leak paths — see git add/commit/push in one command for a workflow that still lets you double-check staged files before every push.
Quick FAQ
Is it safe to store secrets in CI/CD environment variables?
Yes, as long as they’re set in the CI platform’s secret store (not committed in a YAML file) and masked in logs, which most modern CI providers do by default.
Should `.env.example` ever contain a real value?
No. Use placeholders like `replace_me` or clearly fake test values, never a working credential.
What’s the safest way to share a `.env` file with a teammate?
Use a secrets manager or an encrypted channel, never Slack, email, or a shared doc — those are all commonly breached or logged.
Leave a Reply