The Developer's Quarterly · Next.js FoundationsVol. II · February 2026

Next.js Environment Files:How Loading, Priority, and Browser Exposure Actually Work

Next.js environment files follow a specific naming scheme, load order, and browser exposure rule, and understanding those details prevents many staging and production configuration bugs.

Environment files in Next.js look simple at first. You add a .env.local, read a value with process.env, and move on. The trouble starts later, when a value works in development but disappears in production, or when a supposedly secret variable ends up in the browser bundle, or when a staging deployment still points at the wrong API.

Most of those mistakes come from not knowing how Next.js actually handles environment files. The good news is that the rules are consistent: the framework has a defined set of file names, a predictable loading order, and a clear rule for which variables are exposed to client-side code.

Once those rules are understood, environment setup becomes much less mysterious. The real work is not memorizing file names. It is understanding which source wins, when values are read, and which values belong on the server instead of the browser.

The file names Next.js recognizes

Next.js follows a small set of conventional environment file names: .env, .env.local, .env.development, .env.production, .env.test, plus their *.local variants for each mode.

1 Base file

.env is the broadest default layer and works well for shared non-secret defaults.

2 Local override

.env.local is meant for machine-specific overrides and local secrets that should not be committed.

3 Mode-specific files

.env.development, .env.production, and .env.test let values vary by framework mode.

Common project setup
.env .env.local .env.development .env.production

If a file does not match the expected naming scheme, Next.js does not include it in the normal loading process. That is why inventing custom names often creates confusion instead of clarity.

What the files are usually for

The base .env file is a reasonable place for safe defaults shared across environments, while .env.local is best reserved for developer-specific secrets and machine-specific overrides.

Example file contents
# .env APP_NAME=mo-app FEATURE_CONTACT_FORM=true # .env.local DATABASE_URL=postgres://localhost:5432/mo_app_dev SESSION_SECRET=local-dev-secret # .env.development API_BASE_URL=http://localhost:4000 NEXT_PUBLIC_SITE_URL=http://localhost:3000 # .env.production API_BASE_URL=https://api.example.com NEXT_PUBLIC_SITE_URL=https://example.com

That split keeps shared defaults stable while still allowing local overrides and production-specific values to exist without stepping on each other unnecessarily.

Load order is the real rule

The most important behavior is precedence. When the same variable appears in more than one place, the first matching source in Next.js priority order wins. The useful mental model is this: the framework checks process-level variables first, then the more specific env files, and only later falls back to the broad defaults.

Priority Source
1 Existing process.env
2 .env.$(NODE_ENV).local
3 .env.local
4 .env.$(NODE_ENV)
5 .env

So if API_BASE_URL exists in both .env and .env.local, the local file wins. If your deployment platform injects the variable directly into the process environment, that value wins before the files are even considered.

A concrete precedence example

Conflicting values
# .env API_BASE_URL=https://base.example.com # .env.development API_BASE_URL=http://localhost:4000 # .env.local API_BASE_URL=http://127.0.0.1:5000

During next dev, the resolved value will be http://127.0.0.1:5000 because .env.local outranks .env.development, and .env.development outranks .env.

If the deployment platform later injects API_BASE_URL=https://staging-api.example.com directly, that process-level value becomes the effective value instead.

Development, production, and test load different files

The active framework mode changes which env-specific files Next.js uses. next dev reads development-mode files, while next build and next start use production-mode files. Test mode is separate again when the test runner sets it correctly.

Development Uses development-mode env files and usually local service endpoints.
Production build Uses production-mode env resolution during build and runtime startup.
Test Can rely on test-specific files when the test environment is configured correctly.
Staging Is usually still a production-mode build, just with staging values supplied from the environment.

What reaches the browser

This is the security boundary that matters most. In Next.js, only variables whose names start with NEXT_PUBLIC_ are exposed to client-side code. Everything else is intended to remain server-only.

Public versus server-only
DATABASE_URL=postgres://db.internal/my_app JWT_SECRET=super-secret-value NEXT_PUBLIC_SITE_URL=https://example.com NEXT_PUBLIC_SUPPORT_EMAIL=support@example.com

Server code can read all of these values. Client components should only receive the two NEXT_PUBLIC_ variables. If a browser-facing feature seems to require a secret, that usually means the logic belongs on the server instead.

A server and client example

Server-side config
export const serverConfig = { databaseUrl: process.env.DATABASE_URL, jwtSecret: process.env.JWT_SECRET, apiBaseUrl: process.env.API_BASE_URL, };
Client component
'use client'; export function FooterMeta() { return ( <p> Running on {process.env.NEXT_PUBLIC_SITE_URL} </p> ); }

Trying to read process.env.JWT_SECRET from a client component is not a valid design. If the browser needs some derived information, expose only that derived value through a server endpoint or a safe public variable.

Variable expansion is supported

Next.js also supports referencing one environment variable from another. That can be useful when several values share the same host or base path and you want to avoid duplication across env files.

Variable expansion
HOST=example.com NEXT_PUBLIC_SITE_URL=https://$HOST PRICE_LABEL=$25 plan

The first line expands the host reference. The last line shows how to escape a literal dollar sign instead of triggering expansion.

Build-time behavior is where bugs appear

One of the most important Next.js details is that many environment values, especially public values used in client bundles, are resolved at build time. If you build with one set of public values and later promote that exact artifact to another environment, the browser bundle can still contain the old values.

1 Build separately when needed

If staging and production expose different public values, separate builds are often the safer path.

2 Prefer server-side decisions

If the browser does not truly need a value, keep it on the server side and return only derived data.

3 Do not assume promotion is free

A build artifact created with staging public values is not automatically safe to promote unchanged to production.

Centralize environment access

Even when Next.js loads your files correctly, the codebase can still become messy if every module reads process.env directly. A central env module keeps validation and naming rules in one place.

Central env module
const required = (value: string | undefined, name: string) => { if (!value) { throw new Error('Missing environment variable: ' + name); } return value; }; export const env = { appName: process.env.APP_NAME ?? 'mo-app', apiBaseUrl: required(process.env.API_BASE_URL, 'API_BASE_URL'), siteUrl: required(process.env.NEXT_PUBLIC_SITE_URL, 'NEXT_PUBLIC_SITE_URL'), supportEmail: process.env.NEXT_PUBLIC_SUPPORT_EMAIL ?? 'support@example.com', };

That pattern validates required variables early, makes the rest of the codebase easier to read, and prevents environment handling from being scattered across pages, route handlers, helpers, and client components.

Route Handlers are a good server boundary

Route Handlers are a good place to consume server-only environment values because they run on the server boundary. The client receives the API response, not the raw secret or internal configuration value.

Route handler example
import { NextResponse } from 'next/server'; export async function GET() { const apiBaseUrl = process.env.API_BASE_URL; const response = await fetch(apiBaseUrl + '/health'); const data = await response.json(); return NextResponse.json(data); }

What you should usually commit

In many teams, a good default is to commit .env only when it contains safe shared defaults, commit mode-specific files only when they contain safe examples or non-secret defaults, and avoid committing .env.local or other secret local overrides.

The exact policy depends on the project, but the goal stays the same: secrets belong in uncommitted local files or deployment platform settings, not in source control.

Bottom line

Next.js environment files are not complicated, but they are precise. The framework recognizes specific file names, loads them in a clear priority order, and only exposes NEXT_PUBLIC_ variables to browser code.

Once you internalize those rules, configuration becomes much easier to reason about. Local overrides stay local, secrets stay server-side, public values are exposed intentionally, and deployment bugs become much easier to diagnose before they reach production.