In production, Next.js handles environment variables in two different ways. Variables prefixed with NEXT_PUBLIC_ are copied into the browser JavaScript at build time, so changing them requires a rebuild. All other variables stay on the server and are read from process.env when server code runs, but a page that is statically rendered reads them at build time too. Most "my env var is undefined" bugs come from mixing up these two moments. This guide explains the rules and a setup that keeps secrets safe.
The two kinds of variables
NEXT_PUBLIC_ variables |
Server-only variables | |
|---|---|---|
| Available in | Browser and server | Server only (Server Components, Route Handlers, Server Actions, proxy) |
| When the value is fixed | At next build |
When the code runs, unless the page is prerendered at build |
| Change needs | A new build | A restart (or a rebuild for static pages) |
| Safe for secrets | Never | Yes |
NEXT_PUBLIC_ variables are inlined at build time
When you write:
const key = process.env.NEXT_PUBLIC_MAPS_KEY;
next build replaces that expression with the literal string value. The browser bundle contains the value itself, not a reference to an environment variable. Consequences:
- The variable must exist when the build runs. If you add it to your host after deploying, the old bundle still has
undefinedbaked in. Redeploy. - Everyone can read it. Open DevTools and search the JavaScript and it is there. Only put public values here: analytics IDs, public map keys restricted by domain, your site URL.
- Dynamic lookups are not inlined. This does not work in the browser:
// Not inlined, will be undefined in client code
const name = "NEXT_PUBLIC_MAPS_KEY";
const key = process.env[name];
Always reference the full name literally.
Server variables and when they are read
Server-only variables (no prefix) never reach the browser. When they are read depends on how the route is rendered:
- Dynamic routes (they read cookies, headers, search params, or call
connection()) run on every request, soprocess.envis read at runtime. - Static routes are prerendered during
next build, so anyprocess.envvalues used while rendering are captured at build time.
If a page must use the runtime value, for example because you build once and deploy the same artefact to staging and production, opt it into dynamic rendering:
// app/status/page.tsx
import { connection } from "next/server";
export default async function StatusPage() {
await connection(); // render at request time
return <p>Region: {process.env.APP_REGION}</p>;
}
The Next.js environment variables guide covers this runtime behaviour in more detail.
Loading order for .env files
Next.js loads .env files automatically and stops at the first place a variable is found, in this order:
process.env(real environment variables set by your host).env.$(NODE_ENV).local, e.g..env.production.local.env.local(not loaded whenNODE_ENVistest).env.$(NODE_ENV), e.g..env.production.env
Real environment variables always win. That is what you want in production: the host's settings override anything in files.
A practical split:
| File | Commit it? | Use for |
|---|---|---|
.env |
Yes, if it holds only non-secret defaults | Safe defaults shared by the team |
.env.local |
No | Your local secrets |
.env.example |
Yes | Documenting which variables exist, with dummy values |
| Host dashboard | Not in Git | All production values and secrets |
create-next-app adds .env* to .gitignore. If you want to commit .env.example, add an exception line !.env.example.
Keep secrets on the server
Three habits prevent most leaks:
1. Never prefix secrets with NEXT_PUBLIC_. It sounds obvious, but it happens when someone "just needs it to work" in a Client Component. The fix is to move the call to the server (a Route Handler or Server Action), not to expose the key.
2. Mark server modules as server-only. If a Client Component accidentally imports a module that reads secrets, the build fails instead of leaking:
// lib/env.ts
import "server-only";
import { z } from "zod";
const schema = z.object({
DATABASE_URL: z.string().min(1),
SESSION_SECRET: z.string().min(32),
SMTP_PASSWORD: z.string().min(1),
});
export const env = schema.parse(process.env);
Install the marker package with npm install server-only.
3. Validate at startup. The schema above throws a clear error listing missing variables, instead of a vague failure on the first request that needs one.
Build once, deploy many: runtime public config
Because NEXT_PUBLIC_ values are frozen at build, you cannot build one artefact and give it different public values per environment. If you need that, pass values from the server instead:
// app/layout.tsx (Server Component)
import { connection } from "next/server";
import { ConfigProvider } from "./config-provider";
export default async function RootLayout({ children }: { children: React.ReactNode }) {
await connection();
const publicConfig = { apiBase: process.env.PUBLIC_API_BASE ?? "" };
return (
<html lang="en">
<body>
<ConfigProvider value={publicConfig}>{children}</ConfigProvider>
</body>
</html>
);
}
Only pass values that are safe for the browser. Note that calling connection() in the root layout makes every page dynamic, so use this only when you really need per-environment public values.
Environment variables on NewHost
On NewHost you set environment variables per app in the dashboard, where they are stored encrypted, so secrets never need to live in your repository. Because NEXT_PUBLIC_ values are inlined during the build, trigger a new deploy after changing one so the build picks it up.
Preview deploys (Developer plan and up) let you check a branch with its own URL before merging. Keep preview environments pointed at test databases and test API keys, not production ones. For the full deploy flow, see how to deploy a Next.js app from GitHub.
Frequently asked questions
Why is my Next.js environment variable undefined in production?
Usually one of three things: the variable was added after the build (for NEXT_PUBLIC_ values), it is being read in a Client Component without the NEXT_PUBLIC_ prefix, or it was read with a dynamic key like process.env[name]. Set it, reference it literally and redeploy.
Are NEXT_PUBLIC_ variables secret?
No. They are embedded in the JavaScript sent to every visitor. Treat them as public, and use domain or referrer restrictions on any public API keys.
Do I need dotenv with Next.js?
No. Next.js loads .env files itself. For scripts that run outside Next.js, you can use @next/env or Node.js's built-in --env-file flag.
Can I change an environment variable without redeploying?
For server-only variables used by dynamic routes, a restart is enough. For NEXT_PUBLIC_ variables and anything used in statically rendered pages, you need a new build.
Deploying a Next.js app with secrets you want kept safe? NewHost Next.js hosting stores environment variables encrypted, and the production checklist covers the rest.