Next.js Environment Variables

Manage environment variables and configure Next.js for different environments.

TL;DR

  1. 01Use .env.local for environment variables in development.
  2. 02Variables prefixed NEXT_PUBLIC_ are exposed to browser.
  3. 03Server-only variables are only available on the server.

Tips

  1. 01Use .env.example to document which environment variables are needed — commit it to version control.

Warnings

  1. 01Never commit .env.local files — they contain secrets. Use .gitignore to exclude them.

Environment Files

  • Create .env.local for local development variables.
    # .env.local
    DATABASE_URL=postgresql://user:password@localhost/db
    API_SECRET=my-secret-key
  • Create .env.production.local for production secrets — Next.js loads it only in production and it is gitignored by default.
    # .env.production.local
    DATABASE_URL=postgresql://user:password@prod/db
    API_SECRET=production-secret
  • Variables are loaded at build time for static files.
  • Use .env.development to set variables for npm run dev only.
    # .env.development
    NEXT_PUBLIC_API_URL=http://localhost:4000
    LOG_LEVEL=debug
  • Use .env for shared defaults across all environments.
    # .env (committed — no secrets)
    NEXT_PUBLIC_APP_NAME=My App
    NEXT_PUBLIC_SUPPORT_EMAIL=support@example.com
  • Next.js uses first-definition-wins: files are checked highest to lowest priority.
    # Priority (highest to lowest):
    # 1. .env.{NODE_ENV}.local  (e.g. .env.development.local)
    # 2. .env.local             (skipped in test environment)
    # 3. .env.{NODE_ENV}        (e.g. .env.development)
    # 4. .env                   (shared defaults)
    # The FIRST file that defines a variable wins — later files cannot override it.

Public Variables

  • Prefix with NEXT_PUBLIC_ to expose to browser.
    # .env.local
    NEXT_PUBLIC_API_URL=https://api.example.com
    NEXT_PUBLIC_APP_NAME=My App
  • Access in client and server code.
    function Component() {
      const apiUrl = process.env.NEXT_PUBLIC_API_URL;
      return <p>API: {apiUrl}</p>;
    }
  • Use for non-sensitive data only.
  • Public variables are inlined at build time, not at runtime.
    // process.env.NEXT_PUBLIC_API_URL becomes a string literal in the bundle
    const url = process.env.NEXT_PUBLIC_API_URL; // "https://api.example.com"
  • Use for things like Stripe publishable keys or analytics IDs.
    NEXT_PUBLIC_STRIPE_KEY=pk_live_abc123
    NEXT_PUBLIC_GA_ID=G-XXXXXXX

Server-Only Variables

  • Variables without NEXT_PUBLIC_ are server-only.
    # .env.local
    DATABASE_URL=postgresql://...
    API_KEY=secret-key
  • Access only in server-side code.
    export async function GET() {
      const apiKey = process.env.API_KEY;
      // Safe: runs on server only
    }
  • Never available in browser or client components.
  • Use server-only variables in server actions and route handlers.
    "use server";
    
    export async function deleteUser(id: string) {
      await db.user.delete({ where: { id } });
      // process.env.DATABASE_URL is safe here
    }
  • Attempting to access server-only vars in client code returns undefined.
    "use client";
    // process.env.API_KEY is undefined — not leaked to browser

Runtime Config and Validation

  • Expose build-time variables to the client via the env key in next.config.js.
    // next.config.js
    module.exports = {
      env: {
        APP_VERSION: process.env.npm_package_version, // inlined at build time
        FEATURE_FLAG: process.env.FEATURE_FLAG
      }
    };
  • Use serverRuntimeConfig for values available only on the server at runtime.
    // next.config.js
    module.exports = {
      serverRuntimeConfig: {
        mySecret: process.env.MY_SECRET // server-only, not in browser
      },
      publicRuntimeConfig: {
        apiUrl: process.env.NEXT_PUBLIC_API_URL // shared with client
      }
    };
  • Validate required environment variables at startup to catch missing config immediately.
    // lib/env.ts — imported in app/layout.tsx
    const required = ["DATABASE_URL", "AUTH_SECRET"];
    for (const key of required) {
      if (!process.env[key]) throw new Error(`Missing env var: ${key}`);
    }
  • Use the t3-env or envalid library for type-safe, schema-validated environment variables.
    import { createEnv } from "@t3-oss/env-nextjs";
    import { z } from "zod";
    
    export const env = createEnv({
      server: { DATABASE_URL: z.string().url() },
      client: { NEXT_PUBLIC_API_URL: z.string().url() },
      runtimeEnv: process.env
    });
  • Restart the dev server after changing .env files — Next.js does not hot-reload environment variables.
    # After editing .env.local or adding new variables:
    npm run dev  # restart required for new vars to take effect

Secrets and Version Control

  • Never commit .env.local or .env.production.local to version control.
    # .gitignore
    .env.local
    .env.development.local
    .env.test.local
    .env.production.local
  • Commit .env.example as documentation of required variables — never put real values in it.
    # .env.example (safe to commit)
    DATABASE_URL=postgresql://user:password@localhost/db
    NEXT_PUBLIC_API_URL=https://api.example.com
    AUTH_SECRET=
  • Never access process.env in a Client Component for secrets — server-only variables return undefined in the browser and do NOT get bundled.
    "use client";
    // ❌ DATABASE_URL is undefined in the browser — not leaked, just missing
    const url = process.env.DATABASE_URL;
  • Use NEXT_PUBLIC_ only for truly public values — these are inlined as string literals and visible to anyone who reads the JS bundle.
    # Safe to expose
    NEXT_PUBLIC_STRIPE_KEY=pk_live_abc123
    NEXT_PUBLIC_GA_ID=G-XXXXXXX
    
    # Never prefix with NEXT_PUBLIC_
    STRIPE_SECRET=sk_live_secret
    DATABASE_URL=postgres://prod-secret
  • Set environment variables in your CI/CD platform rather than committing .env.production files.
    # Vercel CLI
    vercel env add DATABASE_URL production
    
    # GitHub Actions secret
    # Settings > Secrets > Actions > New repository secret

FAQ