Environment Variables
Environment variables store configuration and secrets. Next.js has a specific system for handling them safely.
The Basics
Create .env.local for local development:
# .env.local
DATABASE_URL="postgresql://localhost/mydb"
API_SECRET="super-secret-key"
NEXT_PUBLIC_API_URL="https://api.example.com"
Access in your code:
// Server-side (Server Components, API routes, Server Actions)
const dbUrl = process.env.DATABASE_URL
// Client-side (only NEXT_PUBLIC_ prefixed)
const apiUrl = process.env.NEXT_PUBLIC_API_URL
The NEXT_PUBLIC_ Prefix
The prefix determines where variables are available:
| Prefix | Server | Client | Bundle |
|---|---|---|---|
| None | ✅ | ❌ | Not included |
NEXT_PUBLIC_ | ✅ | ✅ | Embedded in JS |
// ❌ This will be undefined on the client
const secret = process.env.API_SECRET // undefined
// ✅ This works everywhere
const apiUrl = process.env.NEXT_PUBLIC_API_URL // "https://..."
Environment Files
Next.js loads files in this order (later overrides earlier):
| File | Purpose | Git |
|---|---|---|
.env | All environments | Commit |
.env.local | Local overrides | Ignore |
.env.development | Dev only | Commit |
.env.production | Prod only | Commit |
.env.development.local | Local dev overrides | Ignore |
.env.production.local | Local prod overrides | Ignore |
Security Best Practices
Never Commit Secrets
# .gitignore
.env*.local
Use Different Values Per Environment
# .env.development
DATABASE_URL="postgresql://localhost/mydb_dev"
# .env.production
DATABASE_URL="postgresql://prod-server/mydb_prod"
Validate Required Variables
// lib/env.ts
function getEnvVar(name: string): string {
const value = process.env[name]
if (!value) {
throw new Error(`Missing environment variable: ${name}`)
}
return value
}
export const env = {
DATABASE_URL: getEnvVar('DATABASE_URL'),
API_SECRET: getEnvVar('API_SECRET'),
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || '',
}
Type-Safe Environment Variables
Use Zod for runtime validation:
// lib/env.ts
import { z } from 'zod'
const envSchema = z.object({
DATABASE_URL: z.string().url(),
API_SECRET: z.string().min(32),
NODE_ENV: z.enum(['development', 'production', 'test']),
NEXT_PUBLIC_API_URL: z.string().url(),
})
// This runs at startup
export const env = envSchema.parse({
DATABASE_URL: process.env.DATABASE_URL,
API_SECRET: process.env.API_SECRET,
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
})
Add TypeScript support:
// env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string
API_SECRET: string
NEXT_PUBLIC_API_URL: string
}
}
Common Patterns
API Keys
# Server-side only (no prefix)
STRIPE_SECRET_KEY="sk_live_..."
OPENAI_API_KEY="sk-..."
# Client-side (with prefix)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_..."
Feature Flags
NEXT_PUBLIC_FEATURE_NEW_CHECKOUT="true"
NEXT_PUBLIC_FEATURE_DARK_MODE="true"
const isNewCheckout = process.env.NEXT_PUBLIC_FEATURE_NEW_CHECKOUT === 'true'
Service URLs
# Development
NEXT_PUBLIC_API_URL="http://localhost:3001"
# Production (in .env.production)
NEXT_PUBLIC_API_URL="https://api.myapp.com"
Deployment
Vercel
Set variables in the dashboard or CLI:
vercel env add DATABASE_URL production
Docker
Pass at runtime:
# Dockerfile
ENV NEXT_PUBLIC_API_URL=""
# docker-compose.yml
environment:
- DATABASE_URL=${DATABASE_URL}
- NEXT_PUBLIC_API_URL=https://api.example.com
Summary
- Use
NEXT_PUBLIC_prefix only for client-safe values - Never commit
.env.localfiles - Use environment-specific files for different configs
- Validate required variables at startup
- Consider Zod for type-safe environment validation
- Keep secrets server-side only (no prefix)

