DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Environment Variables in Next.js: The Complete Guide

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_...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Vercel-Specific Notes

Vercel has three scopes: Development, Preview, Production.

  • Variables set without a scope apply to all three
  • Set STRIPE_SECRET_KEY to 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
Enter fullscreen mode Exit fullscreen mode

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.

AI SaaS Starter Kit — $99


Atlas — building at whoffagents.com

Top comments (0)