env.dev

Next.js Environment Variables: Complete Guide

How Next.js handles environment variables: .env files, NEXT_PUBLIC_ prefix, server vs client access, load order, and common production errors.

Last updated:

Next.js has built-in support for environment variables with a clear separation between server-only and client-exposed values. The framework loads .env files automatically, uses the NEXT_PUBLIC_ prefix to control what reaches the browser bundle, and inlines public values at build time. Misunderstanding any of these mechanics is the root cause of most "undefined in production" bugs. This guide covers the full loading order, the prefix rule, how to access env vars across every Next.js context, and the pitfalls that trip up experienced developers.

Which .env files does Next.js load?

Next.js loads environment files in a specific order using @next/env. Every file is optional. Values from files loaded later do not override values from earlier files — the first definition wins.

  1. .env.{NODE_ENV}.local — e.g. .env.development.local (highest priority, git-ignored)
  2. .env.local — local overrides, always loaded except in test environment
  3. .env.{NODE_ENV} — e.g. .env.production
  4. .env — base defaults (lowest priority)

The NODE_ENV value is set automatically: development for next dev, production for next build and next start, and test when set explicitly for test runners.

ini
# .env — shared defaults
DATABASE_URL=postgres://localhost:5432/myapp
NEXT_PUBLIC_APP_NAME=MyApp

# .env.local — developer-specific overrides (git-ignored)
DATABASE_URL=postgres://localhost:5432/myapp_dev
SECRET_KEY=dev-only-secret

# .env.production — production defaults
NEXT_PUBLIC_API_URL=https://api.example.com

What does the NEXT_PUBLIC_ prefix do?

This is the single most important rule in Next.js environment variable handling. Only variables prefixed with NEXT_PUBLIC_ are exposed to the browser. Everything else is server-only and never included in the client JavaScript bundle.

ini
# Server-only — NEVER sent to the browser
DATABASE_URL=postgres://user:pass@db:5432/app
STRIPE_SECRET_KEY=sk_live_abc123

# Client-safe — inlined into the browser bundle at build time
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xyz789
NEXT_PUBLIC_API_URL=https://api.example.com

At build time, Next.js uses DefinePlugin (webpack) or its equivalent in Turbopack to statically replace every occurrence of process.env.NEXT_PUBLIC_* with the literal string value. This means public env vars are baked into the output at build time — changing them requires a rebuild.

Security implication: If you accidentally add NEXT_PUBLIC_ to a secret, that secret will appear in your client bundle in plain text. Audit your .env files carefully. Use our env validator to check for issues.

How do you access env vars in Server Components?

Server Components (the default in the App Router) run exclusively on the server. You have full access to all environment variables via process.env:

tsx
// app/dashboard/page.tsx — Server Component (default)
export default async function DashboardPage() {
  // Server-only vars work fine here
  const dbUrl = process.env.DATABASE_URL;
  const data = await fetch(process.env.INTERNAL_API_URL + '/stats');

  // NEXT_PUBLIC_ vars also work on the server
  const appName = process.env.NEXT_PUBLIC_APP_NAME;

  return <h1>{appName} Dashboard</h1>;
}

Important: Next.js does not allow destructuring process.env because the replacement is a static string substitution. Always use the full process.env.VARIABLE_NAME expression.

typescript
// This will NOT work — destructuring breaks static replacement
const { DATABASE_URL } = process.env; // undefined

// This works
const dbUrl = process.env.DATABASE_URL; // "postgres://..."

How do you access env vars in Client Components?

Client Components (marked with 'use client') only have access to NEXT_PUBLIC_ variables. Server-only variables return undefined:

tsx
'use client';

export function Analytics() {
  // Works — public variable is inlined at build time
  const trackingId = process.env.NEXT_PUBLIC_GA_ID;

  // undefined — server-only vars are stripped from client bundles
  const secret = process.env.API_SECRET; // undefined

  return <script data-id={trackingId} />;
}

How do you use env vars in API Routes and Middleware?

Route Handlers and Middleware run on the server, so all variables are available:

typescript
// app/api/webhook/route.ts
export async function POST(request: Request) {
  const secret = process.env.WEBHOOK_SECRET;
  const signature = request.headers.get('x-signature');

  if (!verify(signature, secret)) {
    return Response.json({ error: 'Invalid' }, { status: 401 });
  }
  return Response.json({ ok: true });
}

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const allowedOrigin = process.env.ALLOWED_ORIGIN;
  // Full access to server-only env vars
  return NextResponse.next();
}

What is the full load order and precedence?

When the same variable is defined in multiple files, the first value found wins. Next.js checks in this order:

  1. process.env (actual shell environment — always wins)
  2. .env.{NODE_ENV}.local
  3. .env.local (skipped when NODE_ENV=test)
  4. .env.{NODE_ENV}
  5. .env

This means a DATABASE_URL set in your shell or CI environment will always override anything in your .env files. For a deeper look at .env file syntax and conventions, see our .env guide.

Why is my env var undefined in production?

This is the most common issue developers hit with Next.js environment variables. The causes differ based on whether the variable is public or server-only.

NEXT_PUBLIC_ var is undefined on the client

  • The variable was not set at build time. Public vars are inlined during next build. Setting them at runtime has no effect.
  • You are using destructuring: const { NEXT_PUBLIC_X } = process.env does not work.
  • You are computing the key dynamically: process.env[key] does not work for public vars because the replacement is static.

Server-only var is undefined

  • The variable is not set in the deployment environment. Check your hosting provider's env var configuration (Vercel, Cloudflare, AWS, etc.).
  • .env.local is not deployed — it is git-ignored by default and only exists on your local machine.
  • You added it to .env.development but production uses .env.production.

How do you add type safety for env vars?

Extend the ProcessEnv interface so TypeScript catches typos:

typescript
// env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    DATABASE_URL: string;
    STRIPE_SECRET_KEY: string;
    NEXT_PUBLIC_APP_URL: string;
    NEXT_PUBLIC_GA_ID: string;
  }
}

For runtime validation, parse your env vars at startup with a schema library:

typescript
// lib/env.ts
import { z } from 'zod';

const serverSchema = z.object({
  DATABASE_URL: z.string().url(),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
});

const clientSchema = z.object({
  NEXT_PUBLIC_APP_URL: z.string().url(),
});

// Validate on import — fails fast if misconfigured
export const serverEnv = serverSchema.parse(process.env);
export const clientEnv = clientSchema.parse({
  NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
});

Best Practices

  • Never prefix secrets with NEXT_PUBLIC_. Double-check every variable before adding the prefix. API keys, database credentials, and signing secrets must stay server-only.
  • Commit a .env.example file. Document every required variable with empty or placeholder values. This file serves as onboarding documentation and can be validated in CI.
  • Validate at build time. Use a schema validator (Zod, Valibot, or similar) to fail the build immediately when required variables are missing.
  • Set production vars in your hosting platform. Do not rely on .env.production for secrets — use Vercel Environment Variables, Cloudflare secrets, or your CI provider's secret store.
  • Keep .env.local out of version control. Next.js adds it to .gitignore by default with create-next-app. Verify this is the case in your repo.
  • Use the full process.env.VAR syntax. Never destructure, never use dynamic keys. The static replacement requires the exact literal expression.

FAQ

Can I change NEXT_PUBLIC_ vars without rebuilding?

No. Public env vars are replaced with literal strings at build time. To change them, you must run next build again. If you need runtime configuration on the client, fetch it from an API route or use __NEXT_DATA__ via getServerSideProps (Pages Router) or pass it as props from a Server Component.

Why does .env.local not load in tests?

By design, Next.js skips .env.local when NODE_ENV=test so that tests produce consistent results across machines. Use .env.test or .env.test.local instead.

Does process.env work in Edge Runtime?

Yes. Both process.env and NEXT_PUBLIC_ variables work in Edge Runtime (Middleware, Edge API Routes). The values are inlined at build time, just like client-side public vars.

How do I use different API URLs per environment?

Define NEXT_PUBLIC_API_URL in each environment file:

ini
# .env.development
NEXT_PUBLIC_API_URL=http://localhost:4000

# .env.production
NEXT_PUBLIC_API_URL=https://api.example.com

Next.js loads the correct file based on NODE_ENV.

Validate your .env files for syntax errors and common mistakes with the env validator tool, or read the comprehensive .env guide for syntax and cross-language usage.

Frequently Asked Questions

Why are my Next.js environment variables undefined in production?

Next.js does not load .env files in production by default. You must set environment variables in your hosting platform (Vercel, AWS, etc.) or inject them at build time. Also, client-side variables must use the NEXT_PUBLIC_ prefix.

What does NEXT_PUBLIC_ do in Next.js?

The NEXT_PUBLIC_ prefix tells Next.js to inline the variable into the client-side JavaScript bundle at build time. Without this prefix, environment variables are only available in server-side code (API routes, getServerSideProps, Server Components).

What is the .env file load order in Next.js?

Next.js loads .env files in this order (later files override earlier ones): .env, .env.local, .env.development/.env.production (based on NODE_ENV), .env.development.local/.env.production.local. The .env.local file is always ignored in test environments.

Was this helpful?

Stay up to date

Get notified about new guides, tools, and cheatsheets.