Next.js environment variables cause more confusing first-deploy bugs than almost anything else. The app works locally, the preview link shows a blank section, and production calls the test database. The cause is nearly always the same: a value was set in one place and read from another, or read at a different time than you expected. This guide explains how Next.js loads environment variables, what changes between development, preview and production, and a setup that keeps secrets out of the browser and out of your repository.
Everything about Next.js behaviour below comes from the official Next.js guide to environment variables (last updated in August 2026 at the time of writing). Hosting behaviour is illustrated with Netlify's and Cloudflare's documentation; other hosts work on the same principles but name things differently.
How Next.js environment variables are loaded
Next.js has built-in support for loading variables from .env files into process.env. Support for .env files and the NEXT_PUBLIC_ prefix was introduced in version 9.4.0, so any current project has it.
There are several files, and each has a purpose:
| File | Loaded when | Commit it? |
|---|---|---|
| .env | Every environment | Defaults only, never secrets |
| .env.development | Running next dev | Defaults only |
| .env.production | Running next build and next start | Defaults only |
| .env.test | Tests, when NODE_ENV is test | Yes, per the Next.js docs |
| .env.local | Every environment except test | No |
| .env.development.local and similar | That environment only | No |
The Next.js docs carry a clear warning here: the default create-next-app template adds all .env files to .gitignore, and "you almost never want to commit these files to your repository." The one exception they describe is .env.test, which should be included in the repository, while .env.test.local should not. If you do commit default files, keep them free of anything secret and treat them as documentation of which keys exist. A .env.example file with key names and empty values does the same job with less risk.
Load order
When the same variable is defined in more than one place, Next.js looks in these locations in order and stops at the first match:
- process.env (values already set by your shell or host)
- .env.development.local or .env.production.local, depending on NODE_ENV
- .env.local, which is not checked when NODE_ENV is test
- .env.development or .env.production
- .env
Two consequences follow. First, a value your hosting platform sets always beats any file. Second, .env.local overrides the committed defaults on your machine, which is exactly what you want for personal keys.
The allowed values for NODE_ENV are production, development and test. If you leave it unset, Next.js assigns development when you run next dev, and production for all other commands. That means next build is always a production build, which matters for previews, as explained below.
Small details that catch people out
- If your project uses a src folder, the .env files must stay in the project root. Next.js loads them only from the parent folder, not from src.
- Next.js expands variables that reference other variables with a dollar sign. If a real value contains a dollar sign, such as a generated password, escape it with a backslash or it will be treated as a reference.
- Multiline values, such as private keys, are supported inside double quotes.
- Tools that run outside Next.js, such as an ORM config or a test runner, do not get these values automatically. The docs recommend the @next/env package and its loadEnvConfig function, which Next.js itself uses internally.
Server variables and NEXT_PUBLIC_ variables
This is the single most important rule. By default, environment variables are only available on the server. The Next.js docs explain that non-NEXT_PUBLIC_ variables are only available in the Node.js environment, so they are not accessible to the browser.
To use a value in client-side code, you prefix it with NEXT_PUBLIC_. Next.js then "inlines" the value at build time into the JavaScript bundle that is delivered to the client, replacing every reference with a hard-coded value.
Read that carefully, because it has two consequences.
Anything with the prefix is public. It is written into JavaScript files any visitor can download. An analytics ID or a public site URL is fine. A database password, a payment provider's secret key or an AI API key must never carry the prefix. If you notice you have done this, renaming the variable is not enough: the old value has already been published, so rotate the key at its provider.
Public values are frozen when you build. The docs state that after being built, your app will no longer respond to changes to these variables. If you change a NEXT_PUBLIC_ value in your host's dashboard, nothing happens until you build again. If you build one image and deploy it to several environments, all NEXT_PUBLIC_ values keep whatever they were at build time.
There is one more trap. Only direct references are inlined. If code looks up the variable name dynamically, for example by storing the name in another variable first, the docs note it will not be inlined and the browser gets nothing.
Reading values at runtime
Server-side variables without the prefix can be read when a request is handled. The Next.js docs show that reading process.env during dynamic rendering, after calling the connection function from next/server or using request-time APIs such as cookies and headers, evaluates the variable at runtime. That allows a single built image to be promoted through several environments with different values. If the browser needs a value that must change without a rebuild, the docs suggest providing it through your own API rather than through the prefix.
Development, preview and production
These three environments mean different things in Next.js and on your host, and most confusion comes from mixing the two meanings.
Development
On your machine, next dev sets NODE_ENV to development and loads .env.development, then your .env.local. Put your personal keys, usually for test-mode accounts and a development database, in .env.local. Restart the dev server after editing it.
Preview
Next.js has no "preview" value for NODE_ENV. A preview deployment runs next build, so as far as Next.js is concerned it is production, and .env.production applies. The difference between preview and production has to come from your hosting platform.
Netlify, for example, defines separate deploy contexts, including Production, Deploy Previews (for pull or merge requests), Branch deploys and Local development. By default a variable has one value for all contexts, but you can set a different value for each one, and even for a specific branch or a branch pattern. That is where you give previews a staging database and test-mode payment keys, so a reviewer clicking around a pull request cannot touch real customer data.
Production
Production should get its values from the host, not from files. Netlify's docs recommend setting sensitive values in the UI, CLI or API rather than in its configuration file, because that file lives in your repository. They also note that variables set in the configuration file override those set elsewhere, which is worth knowing when a value refuses to change.
The other rule: changes need a new deploy. Netlify's docs state that environment variable changes require a build and deploy to take effect. Combined with Next.js inlining public values at build time, this means the safe habit after changing any variable is to redeploy and check.
A setup that holds up
| Value | Development | Preview | Production |
|---|---|---|---|
| Database URL | Local or dev database in .env.local | Staging database, set on host | Production database, set on host |
| Payment secret key | Test key in .env.local | Test key, set on host | Live key, set on host |
| AI provider API key | Your own key in .env.local | Separate key with a low limit | Production key |
| NEXT_PUBLIC_SITE_URL | Local address | Preview URL or staging domain | Your real domain |
Some practices that make this table reliable:
- Use separate keys per environment where the provider allows it. If a preview key leaks, you revoke one key without breaking production.
- Mark secrets as secret on the host. Netlify can flag variables as containing secret values for extra restrictions. Cloudflare's docs explain that secret values are not visible in Wrangler or the dashboard after you define them, although to the running Worker there is no difference between a variable and a secret.
- Keep local files out of Git. Cloudflare's docs say .dev.vars and .env files should not be committed and recommend adding them to .gitignore. The same applies to every .env.local in a Next.js project.
- Fail early when a value is missing. Validate required server variables when the app starts, so a missing key produces a clear error instead of a broken page in production. The Next.js docs point to the instrumentation register function for code that runs on server startup.
A quick debugging checklist
When a value is wrong or empty, work through this list in order:
- Is the variable defined for the environment you are looking at, including the right deploy context on the host?
- Was it changed after the last build? Redeploy.
- Is it needed in the browser? It needs the NEXT_PUBLIC_ prefix, and it should then not be a secret.
- Is it referenced directly, not through a dynamic lookup?
- Is a higher-priority source, such as a host setting or .env.local, overriding the value you edited?
- Is the .env file in the project root rather than inside src?
Missing environment values are one of the items in our pre-launch checklist for AI-generated code, and ignoring the env file is one of the five mistakes first-time AI app builders make. If you are still choosing a framework, why Next.js is the right base for AI-generated apps covers the wider trade-offs.




