Environment Variables in Next.js: The Complete Guide
Next.js has a specific environment variable system that trips up developers regularly. Here's exactly how it works — file precedence, client vs server, and production gotchas.
The Two Types of Variables
Server-only (default): Available in Route Handlers, Server Components, getServerSideProps, middleware. Never sent to the browser.
Client-accessible: Must be prefixed with NEXT_PUBLIC_. Baked into the JavaScript bundle at build time. Visible in browser DevTools.
# Server-only (secrets go here)
DATABASE_URL=postgresql://...
STRIPE_SECRET_KEY=sk_live_...
ANTHROPIC_API_KEY=sk-ant-...
# Client-accessible (no secrets)
NEXT_PUBLIC_APP_URL=https://myapp.com
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_POSTHOG_KEY=phc_...
File Precedence (Most to Least Priority)
.env.local # Always loaded, ignored by git (put secrets here)
.env.[environment] # .env.development, .env.production, .env.test
.env # Base defaults, committed to git
For a value in both .env and .env.local, .env.local wins.
What goes where:
-
.env— non-secret defaults committed to git (NEXT_PUBLIC_APP_NAME=MyApp) -
.env.local— secrets that never leave your machine or CI -
.env.production— production-specific non-secret values -
.env.test— test environment overrides
.gitignore — What to Include
# Environment files with secrets
.env.local
.env.*.local
# Safe to commit:
# .env (base defaults only, no secrets)
# .env.development (dev-specific non-secrets)
# .env.production (prod-specific non-secrets)
Never commit .env.local. Never put secrets in .env (it gets committed).
Accessing Variables
In Server Components and Route Handlers:
// Works — server-side only
const apiKey = process.env.ANTHROPIC_API_KEY;
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
In Client Components:
'use client';
// Works — NEXT_PUBLIC_ only
const appUrl = process.env.NEXT_PUBLIC_APP_URL;
// undefined at runtime — server-only var not sent to browser
const secret = process.env.ANTHROPIC_API_KEY; // ← undefined
Runtime vs Build-Time
This is the most common source of confusion.
NEXT_PUBLIC_* variables are replaced at build time by the Next.js compiler. They become literal strings in the bundle:
// What you write:
const url = process.env.NEXT_PUBLIC_APP_URL;
// What Next.js compiles to:
const url = "https://myapp.com";
Consequence: If you change a NEXT_PUBLIC_ variable, you must rebuild. Changing it in your hosting provider's env vars dashboard and redeploying isn't enough — you need a fresh build.
Server-side variables are read at runtime from the process environment. No rebuild needed when they change.
Validating Variables at Startup
Fail fast if required variables are missing:
// lib/env.ts
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
export const env = {
databaseUrl: requireEnv('DATABASE_URL'),
stripeSecretKey: requireEnv('STRIPE_SECRET_KEY'),
anthropicApiKey: requireEnv('ANTHROPIC_API_KEY'),
nextAuthSecret: requireEnv('AUTH_SECRET'),
} as const;
Or use zod for type coercion:
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
AUTH_SECRET: z.string().min(32),
NODE_ENV: z.enum(['development', 'production', 'test']),
});
export const env = envSchema.parse(process.env);
Vercel-Specific Notes
Vercel has three scopes: Development, Preview, Production.
- Variables set without a scope apply to all three
- Set
STRIPE_SECRET_KEYto test key for Development/Preview, live key for Production -
NEXT_PUBLIC_*changes require redeployment (new build) to take effect
Template File
Commit a .env.example with all required variables and placeholder values:
# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
# Auth
AUTH_SECRET=generate-with-openssl-rand-base64-32
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# AI
ANTHROPIC_API_KEY=sk-ant-...
# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
This documents what's needed without leaking real values.
Pre-Configured in the Starter Kit
The AI SaaS Starter Kit includes .env.example with all required variables, Zod validation, and lib/env.ts that fails fast on missing config.
Atlas — building at whoffagents.com
Top comments (0)