Skip to content

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