Environment Variables

Files and Priority

FileLoaded whenCommit to git?
.envAlways (defaults)Yes, no secrets
.env.development / .env.productionnext dev / next build+startYes, no secrets
.env.localAlways except tests; overrides the aboveNo
Real environment variablesAlways; highest priority—

NEXT_PUBLIC_ Is Inlined at Build Time

NEXT_PUBLIC_* values are copied into the JavaScript bundle during next build. Changing them later on the server has no effect until you rebuild. Server-only variables are read at runtime.

Example

What the bundler does to client code, in miniature:

const buildEnv: Record<string, string> = {
  NEXT_PUBLIC_SITE_URL: 'https://shopease.in',
  NEXT_PUBLIC_RAZORPAY_KEY_ID: 'rzp_live_abc123',
  DATABASE_URL: 'postgresql://user:secret@db:5432/shop',
};

function inlinePublicEnv(source: string): string {
  return source.replace(/process\.env\.(\w+)/g, (match, name: string) =>
    name.startsWith('NEXT_PUBLIC_') ? JSON.stringify(buildEnv[name] ?? '') : 'undefined');
}

const clientCode = [
  "const site = process.env.NEXT_PUBLIC_SITE_URL;",
  "const key = process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID;",
  "const db = process.env.DATABASE_URL;",
].join('\n');
console.log(inlinePublicEnv(clientCode));

Output

const site = "https://shopease.in";
const key = "rzp_live_abc123";
const db = undefined;

Note: Never prefix a secret with NEXT_PUBLIC_. Anyone can read it in the browser's JS files.

Validate at Startup

// lib/env.ts
import 'server-only';
import { z } from 'zod';

export const env = z
  .object({
    DATABASE_URL: z.string().url(),
    AUTH_SECRET: z.string().min(32),
    RAZORPAY_KEY_SECRET: z.string().min(10),
  })
  .parse(process.env); // crashes early with a clear message if something is missing