Next Env & Config¶
NEXT_PUBLIC_ is not a suggestion - it's a compile-time inlining bomb that embeds your variables into the JavaScript bundle shipped to every browser
Next.js handles environment variables differently than plain Node.js. The NEXT_PUBLIC_ prefix determines whether a variable is available at runtime in the browser or only on the server at build time. Confuse them and your database credentials end up in someone's devtools. No amount of framework security fixes that
.env files¶
Next.js loads environment variables from these files (in order of precedence):
.env.local # local overrides (never commit to git)
.env.development # development environment
.env.production # production environment
.env # default - all environments
Precedence (highest first): 1. process.env (runtime environment) 2. .env.local 3. .env.development or .env.production (depending on NODE_ENV) 4. .env
# .env - committed to git (defaults only)
DATABASE_URL="postgresql://localhost:5432/myapp"
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
# .env.local - NOT committed to git (secrets)
DATABASE_URL="postgresql://prod-user:password@prod-host:5432/myapp"
AUTH_SECRET="super-secret-key-change-in-production"
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"
# .env.production - committed to git (production defaults)
NEXT_PUBLIC_SITE_URL="https://myapp.com"
NEXT_PUBLIC_API_URL="https://api.myapp.com"
Add .env.local to .gitignore. It's there by default:
# .gitignore
.env*.local
NEXT_PUBLIC_ prefix - client leaks¶
The NEXT_PUBLIC_ prefix makes a variable available in the browser bundle
// This variable gets inlined into the JavaScript bundle
// Available everywhere - pages , components , API routes
const apiUrl = process.env.NEXT_PUBLIC_API_URL
// Browser sees: const apiUrl = "https://api.myapp.com"
// It's literally in the source code
Every variable WITHOUT NEXT_PUBLIC_ is server-only:
// Server-only - NOT available in browser
// Components using these will fail at build time if they're Client Components
const dbUrl = process.env.DATABASE_URL // undefined in browser
const secretKey = process.env.AUTH_SECRET // undefined in browser
const dbPassword = process.env.DB_PASSWORD // undefined in browser
The security rule is simple:
- API keys for third-party services - NO
NEXT_PUBLIC_prefix - Database credentials - NO
NEXT_PUBLIC_prefix - Auth secrets and tokens - NO
NEXT_PUBLIC_prefix - Public-facing URLs or IDs - OK with
NEXT_PUBLIC_prefix - Google Analytics ID - OK with
NEXT_PUBLIC_prefix - Feature flags for client - OK with
NEXT_PUBLIC_prefix
Common mistake that leaks secrets:
// ❌ WRONG - this leaks in client bundle
const apiKey = process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY
// ✅ CORRECT - server-only , accessed via route handler or Server Action
const apiKey = process.env.STRIPE_SECRET_KEY
build-time inlining¶
Environment variables are inlined at build time. This means:
NEXT_PUBLIC_API_URL=https://api.myapp.com
Becomes a literal string in the JavaScript bundle:
// In the compiled bundle:
const apiUrl = "https://api.myapp.com"
This means:
- Different builds = different values - change a
NEXT_PUBLIC_variable and you must rebuild - No runtime switching - you can't change
NEXT_PUBLIC_values without redeploying - Bundle inspection - anyone can read your
NEXT_PUBLIC_variables by viewing the source
# After building , check what leaked:
grep -r "NEXT_PUBLIC_" .next/static/
# This shows all inlined NEXT_PUBLIC_ values in the bundle
# If you see DATABASE_URL or AUTH_SECRET here , fix it NOW
runtime config (server-side)¶
Server-only environment variables are read at runtime (not inlined)
// Server Components - read env vars at runtime
// src/app/api/config/route.ts
export async function GET() {
return Response.json({
nodeEnv: process.env.NODE_ENV,
dbUrl: process.env.DATABASE_URL?.slice(0, 20) + '...', // don't expose full URL
// DON'T expose secrets in API responses
})
}
Server-only env vars are available in: * Server Components (when rendering on the server) * Route handlers (route.ts) * Server Actions ('use server' functions) * next.config.ts * middleware.ts * getServerSideProps (Pages Router - legacy)
They are NOT available in: * Client Components (will be undefined) * Browser JavaScript * getStaticProps (runs at build time)
next.config.ts runtime config¶
For values that need to be configurable at runtime (not build time) and accessible on the server:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
serverRuntimeConfig: {
// Will only be available on the server side
databaseUrl: process.env.DATABASE_URL,
authSecret: process.env.AUTH_SECRET
},
publicRuntimeConfig: {
// Will be available on both server and client
// (prefer NEXT_PUBLIC_ env vars instead - simpler)
}
}
export default nextConfig
In practice , just use environment variables directly. serverRuntimeConfig and publicRuntimeConfig are legacy patterns from Pages Router
environment validation at startup¶
Validate required environment variables when your app starts. Fail fast instead of crashing at runtime
// src/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'),
AUTH_SECRET: getEnvVar('AUTH_SECRET'),
GITHUB_CLIENT_ID: getEnvVar('GITHUB_CLIENT_ID'),
GITHUB_CLIENT_SECRET: getEnvVar('GITHUB_CLIENT_SECRET'),
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'
}
// Use it in your app
import { env } from '@/lib/env'
await prisma.$connect(env.DATABASE_URL)
The app throws a clear error at startup if a required variable is missing , instead of failing with a cryptic database connection error at 3AM
Vercel environment variables¶
When deploying on Vercel , set environment variables in the project dashboard:
- Plain - available in the deployment environment (server-side)
- Secret - encrypted , not visible in the dashboard after saving (server-side)
- Preview - available in preview deployments
- Production - available in production only
# Vercel CLI to set env vars
vercel env add DATABASE_URL production
vercel env add AUTH_SECRET production
vercel env add NEXT_PUBLIC_SITE_URL production
NEXT_PUBLIC_ variables get inlined into the build. Changing them requires a new deployment
prerequisites¶
next_11_middleware.md - you should understand middleware and edge runtime
next → next_13_deploy.md