How does Next.js handle environment variables (.env.local vs .env.production)
Next.js loads `.env` files at build time per environment: `.env` (all), `.env.local` (all, gitignored), `.env.development` / `.env.production` (per NODE_ENV). `NEXT_PUBLIC_*` are inlined into the client bundle; everything else is server-only. `.env.local` overrides — for secrets and local config. Build-time inlining means changing public vars requires a rebuild.
File precedence
Loaded in order; later overrides earlier (for the same key):
.env ← shared defaults
.env.development ← when NODE_ENV=development (dev / next dev)
.env.production ← when NODE_ENV=production (next build / next start)
.env.test ← when NODE_ENV=test
.env.local ← always loaded, overrides above (except during NODE_ENV=test)
.env.development.local
.env.production.localConvention: commit .env, .env.development, .env.production. Gitignore everything ending in .local — those hold secrets and per-developer overrides.
Public vs server-only
# .env.production
DATABASE_URL=postgres://... # server-only
NEXT_PUBLIC_API_URL=https://api.example.com # inlined into client bundle- **
NEXT_PUBLIC_* — inlined into the JS bundle at build time. Visible to every visitor. Don't put secrets here. - Everything else — only available on the server (API routes, getServerSideProps, server components).
Access in code
// Server-side (route handler, server component)
const dbUrl = process.env.DATABASE_URL;
// Client-side (any "use client" component)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;Trying to read process.env.DATABASE_URL in a client component returns undefined — Next.js strips it.
Build-time vs runtime
process.env.NEXT_PUBLIC_* is inlined at build time. You can't change the public vars without rebuilding the app. For runtime config on Vercel / serverless, use:
- A runtime config endpoint the client fetches on mount.
- The
NEXT_PUBLIC_*vars at deploy time (rebuild per env).
Production deployment
On Vercel / Netlify / similar:
- Set env vars in the dashboard (per environment).
- They're injected during build (
next build). - For server-only vars, they're also available at runtime (the Node process reads them).
Edge / runtime variants
For edge runtime or runtime config:
export const runtime = "edge";
// process.env still works for vars set in deploy configSecurity
- Never commit secrets to
.envor.env.production. Use.env.local(gitignored) for local dev; deploy dashboard for prod. - Don't prefix secrets with
NEXT_PUBLIC_— they'll end up in the client bundle. - Audit the client bundle for accidentally-public vars:
grep NEXT_PUBLIC .next/static -r.
Test mode
.env.test loads on NODE_ENV=test. .env.local is skipped under test (so CI doesn't pull dev secrets).
Common pitfalls
- Putting secrets in
NEXT_PUBLIC_*by accident. - Expecting client-side changes to env without rebuild.
.env.localnot gitignored — committed secrets.- Trying to use
process.envfor runtime config that should be fetched.
Interview framing
"Next.js layers .env files: .env (shared) → .env.development / .env.production (per NODE_ENV) → .env.local (always, overrides, gitignored). NEXT_PUBLIC_* vars get inlined into the client bundle at build time — anyone can see them. Everything else is server-only. So secrets live in .env.local for dev and in the deployment dashboard for prod. The big mistake is shipping a secret with NEXT_PUBLIC_ prefix and never noticing. Public vars require rebuild to change — for true runtime config, fetch from a config endpoint."
Follow-up questions
- •Why isn't .env.local used in test mode?
- •How would you do runtime config?
- •What does NEXT_PUBLIC_ actually do?
Common mistakes
- •Secrets in NEXT_PUBLIC_*.
- •.env.local committed.
- •Expecting hot-reload of env without restart.
Performance considerations
- •Public vars inlined → no runtime cost. Server-only stay in the Node process.
Edge cases
- •Edge runtime env access.
- •Build-time vs runtime mismatch on Vercel preview deployments.
- •Vars referenced in static export pages.
Real-world examples
- •Vercel deployment dashboards, env var migrations, Next.js docs.