Working with a modern framework like Next.js means managing a lot of moving parts, from API keys and database strings to feature flags. Hardcoding these values directly into your components is a recipe for disaster—it's insecure and makes switching between development and production a nightmare.
This is where environment variables come in. They act as a secure vault for your application's configuration, keeping sensitive data completely separate from your source code.
How Next.js Handles Environment Variables
Think of environment variables as a set of instructions you give your app depending on where it's running. For your local machine, you might point to a local database. For production, you’ll point to the live one. You can swap these instructions out without ever changing the underlying code.
Ever since version 9.4, Next.js has had a fantastic built-in system for this using simple .env files. It’s an approach that has become an industry standard for a reason. In fact, recent studies show that around 89% of production Next.js apps rely on this file-based method for configuration. It's a simple, powerful way to manage the different stages of a project's life. You can read more about how this has changed the game in these insights on environment variables.
The Loading Order: Which .env File Wins?
Next.js doesn't just read one file; it looks for a specific set of .env files and loads them in a particular order. This is called precedence, and understanding it is the key to avoiding frustrating configuration bugs where you know you set a variable, but it's not showing up.
Here’s the golden rule: local files always win.
The most powerful file in this system is .env.local. Any variable you define here will override the same variable from any other file. This makes it the perfect spot for your personal API keys or local database passwords—stuff that should never, ever be committed to your project's Git repository.
This hierarchy is designed to be intuitive, with specific, local settings taking priority over general, shared ones.
As the diagram shows, .env.local sits at the top, ensuring your local setup is always respected without interfering with the project's defaults.
Key Takeaway: The system creates a clean separation:
.envfor shared defaults,.env.developmentor.env.productionfor environment-specific settings, and.env.localfor your private, local-only secrets.
To give you a handy reference, the table below breaks down exactly how Next.js prioritizes these files.
Next.js Environment Variable File Precedence
This table outlines the standard .env files used by Next.js, their intended purpose, and the order in which they are loaded. Understanding this hierarchy is key to avoiding configuration conflicts.
| Filename | Environment | Purpose | Precedence (1 = Highest) |
|---|---|---|---|
.env.local |
All | Local overrides and secrets. Never commit to Git. | 1 |
.env.development |
Development | Default values for the development environment. | 2 |
.env.production |
Production | Default values for the production environment. | 2 |
.env |
All | Global default values for all environments. | 3 |
When a variable is defined in multiple files, the one with the highest precedence (1) is the one Next.js will use. This simple but effective system gives you total control over your application's configuration across every environment.
Securing Your App with Public and Private Variables
When you're building a Next.js app, one of the first and most important things to get right is the separation between what the server knows and what the browser sees. This isn't just a nice-to-have convention; it's a hard-coded security boundary designed to stop you from accidentally leaking sensitive secrets.
I like to think of it like a restaurant. The kitchen is your server—a secure, private space. It’s where you keep your secret recipes, your supplier contacts, and your financial records. These are your private environment variables, things like a DATABASE_URL, an API_SECRET_KEY, or a STRIPE_PRIVATE_KEY.
These secrets must never leave the kitchen. They are meant exclusively for your server-side code, which runs in API Routes or during server-side rendering with getServerSideProps.
The NEXT_PUBLIC_ Gatekeeper
Out in the dining area, every customer gets a menu. It contains public information—the restaurant's address, hours, and a list of available dishes. In Next.js, these are your public environment variables.
To make a variable public and safe to use in the browser, you have to explicitly prefix it with NEXT_PUBLIC_.
Key Takeaway: The
NEXT_PUBLIC_prefix is a direct order to Next.js. It tells the build process, "This variable is safe for public consumption. Go ahead and bake its value right into the JavaScript that gets sent to the user's browser."
If a variable doesn't have this prefix, Next.js flat-out refuses to expose it to the client. This is a deliberate, protective measure. If you try to access process.env.MY_API_KEY in a React component, you'll just get undefined, and your secret stays safe.
Why This Distinction Is Non-Negotiable
Getting this public/private boundary wrong is a massive security blind spot. Accidentally exposing a private key, like a database connection string or a payment gateway secret, is the digital equivalent of leaving the keys to your entire business on a table for anyone to grab.
This isn't just a theoretical problem. Improper variable exposure accounts for a staggering 34% of all configuration-related security breaches in JavaScript applications. The NEXT_PUBLIC_ convention is your first line of defense, and it's so critical that 91% of security audits on Next.js apps now specifically check for its correct usage. You can dive deeper into the official Next.js security guidelines here.
Here’s how this looks in a real-world .env.local file:
This is a PRIVATE variable, only available on the server.
It's used in API routes or for server-side rendering.
DATABASE_URL="postgresql://user:password@host:port/db"
This is a PUBLIC variable, available on both the server AND the client.
It's safe to use anywhere, including React components.
NEXT_PUBLIC_GA_TRACKING_ID="G-XXXXXXXXXX"
And here is how that plays out in your code:
Server-Side Access (e.g., in an API route):
On the server, you have access to everything.// pages/api/user.js
export default function handler(req, res) {
// Both variables are accessible here
console.log(process.env.DATABASE_URL); // "postgresql://…"
console.log(process.env.NEXT_PUBLIC_GA_TRACKING_ID); // "G-XXXXXXXXXX"// … database logic using the private URL
}Client-Side Access (e.g., in a React component):
In the browser, only the public variable is defined.// components/Analytics.js
function Analytics() {
// THIS WILL BE UNDEFINED IN THE BROWSER!
console.log(process.env.DATABASE_URL);// This works perfectly because of the prefix.
console.log(process.env.NEXT_PUBLIC_GA_TRACKING_ID);return
…;
}
This simple but powerful mechanism is foundational to building a secure app. Always pause and ask: "Does this variable really need to be in the browser?" By treating every variable as private by default, you'll build a much safer and more robust Next.js application from day one.
Practical Setup and Usage in Your Next.js Project
Alright, theory is great, but let's get our hands dirty. This is where we take the concepts and plug them directly into a real Next.js project. We'll walk through the entire process, starting with the simple, everyday setup and then graduating to a bulletproof, type-safe configuration.
The most common starting point is your local development environment. Simply create a file named .env.local at the root of your project. Think of this file as your personal, local-only notepad for secrets. It's crucial that this file is listed in your .gitignore and never committed to version control.
Inside your new .env.local file, you can define your variables like this:
This is a private key, only for the server
DATABASE_URL="mongodb://localhost:27017/my_local_db"
API_SECRET="super-secret-key-for-dev"
This is a public key, safe for the browser
NEXT_PUBLIC_API_URL="http://localhost:3000/api"
Once that's saved, Next.js will automatically load these variables into process.env. Just remember to kill and restart your dev server (next dev) for the changes to register.
Accessing Variables on the Server and Client
Now, how do you actually use these? This is where Next.js enforces a critical security boundary.
On the server—inside API Routes, getServerSideProps, or Route Handlers—you have full access. This is your secure "back of house" where you can safely handle sensitive data like database connection strings and private API keys.
For instance, an API route might look like this:
// app/api/products/route.js
export async function GET(req) {
// Accessing both private and public variables is perfectly fine here.
const dbConnectionString = process.env.DATABASE_URL;
const secretKey = process.env.API_SECRET;
console.log('Connecting to:', dbConnectionString);
// You might use the secret key to authenticate with another service…
return Response.json({ message: 'Data fetched successfully!' });
}
On the other hand, in client-side React components—the "front of house" visible to your users—you can only access variables prefixed with NEXT_PUBLIC_. If you try to access process.env.DATABASE_URL in a component, you'll just get undefined. It's a built-in safety net.
Here’s how you’d use a public variable in a React component to fetch data:
// components/ProductList.js
'use client';
import { useEffect, useState } from 'react';
function ProductList() {
const [products, setProducts] = useState([]);
// This is the ONLY way to get an env var on the client.
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
useEffect(() => {
if (!apiUrl) return;
fetch(`${apiUrl}/products`)
.then(res => res.json())
.then(data => setProducts(data.products));
}, [apiUrl]);
return (
// … your component JSX to render the products
);
}
This deliberate separation is one of the best security features in Next.js. It makes it nearly impossible to accidentally leak a sensitive key to the browser.
Creating a Type-Safe Configuration with TypeScript and Zod
Using process.env directly works, but it has a few sharp edges. A simple typo in a variable name (process.env.DATABASE_ULR) won't cause an error; it will just be undefined at runtime, leading to frustrating bugs. We can do better.
By introducing TypeScript and a validation library like Zod, we can create a system that checks our environment variables when the app builds, not when it's already running.
First, add Zod to your project: npm install zod.
Next, create a dedicated file (e.g., src/lib/env.ts) to define a schema for your variables.
Key Insight: Validating environment variables at build time means your application will fail fast. If a required variable is missing or malformed, the build stops. This prevents a broken, misconfigured app from ever being deployed.
Here’s how you can define a schema to parse and validate process.env:
// src/lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url("Database URL must be a valid URL."),
API_SECRET: z.string().min(1, "API Secret cannot be empty."),
NEXT_PUBLIC_API_URL: z.string().url("Public API URL must be a valid URL."),
});
// This will throw a detailed error at build time if validation fails
export const env = envSchema.parse(process.env);
Zod now ensures that DATABASE_URL is a valid URL and API_SECRET isn't empty. If anything is wrong, the build process fails with a clear error message telling you exactly what's missing.
With this setup, you replace all instances of process.env.SOME_VAR with your new, validated env object.
import { env } from '@/lib/env';
// In a server-side file
const dbUrl = env.DATABASE_URL; // 100% type-safe and validated!
This simple change brings autocompletion, type-checking, and peace of mind, knowing your configuration is solid before your code even ships. If you're new to the framework, our guide on how to properly install Next.js is a great resource for getting your project foundation right.
Understanding Build-Time Versus Runtime Variables
One of the trickiest parts of working with environment variables in Next.js is getting a handle on when they’re actually available to your code. It all boils down to a single, critical distinction: are they loaded at build time or at runtime? Getting this wrong is a common source of bugs and deployment headaches.
By default, Next.js ingests environment variables at build time. When you run the next build command, Next.js scans your .env files and your code. It finds every instance of process.env.YOUR_VAR and literally replaces it with the variable's value.
Think of it as a permanent "find and replace" operation. The final JavaScript bundles that get sent to the browser don't contain process.env.NEXT_PUBLIC_API_URL anymore. Instead, the string "https://api.yourapp.com" is hardcoded right into the file.
Build-Time Injection: The Default Behavior
This default approach is fantastic for performance. By "baking" the values directly into the static files, Next.js creates a highly optimized build. The browser doesn't have to do any extra work to look up configuration values because they're already there. This works perfectly for things that rarely change, like a public Google Analytics ID.
But here's the catch: this efficiency comes at the cost of flexibility. Once your application is built, those variables are set in stone. Need to point your staging environment to a different API? You can't just flip a switch. You have to run a completely new build.
This becomes a real problem in modern development workflows. If you’re using CI/CD pipelines to promote a single Docker image from staging to production, the build-time model simply doesn't work. You’d need a separate image for each environment, which defeats the purpose.
Introducing Runtime Variables for Modern Deployments
This is exactly where runtime variables save the day. A runtime variable isn't read when you build the app; it's read when the app starts up on the server or even while it's handling a live request. This is the flexibility modern, container-based deployments demand.
To make this work, you have to shift your thinking. Instead of letting Next.js inject the variable at build time, you need to access it on the server when a request comes in.
Key Insight: To use runtime variables, your code must run on the server at request time. In modern Next.js, this means using them inside Server Components, Route Handlers, or older data fetching methods like
getServerSidePropsin the Pages Router.
When your code executes in a server-side context, it can read the environment variables from the host machine—be it a Docker container, a serverless function, or a virtual machine. This lets you change a DATABASE_URL or an API_KEY by simply updating the variable in your deployment environment and restarting the server. No rebuild required.
This dynamic, server-first approach is a core part of what makes Next.js so powerful. It contrasts sharply with purely static methods, which you can learn more about in our guide to Static Site Generation (SSG).
Build-Time vs. Runtime Variables Comparison
So, which one should you use? The answer almost always depends on your specific deployment strategy and the nature of the variable itself. This table breaks down the core differences to help you decide.
| Attribute | Build-Time Variables (Default) | Runtime Variables (Advanced) |
|---|---|---|
| When Loaded | During the next build process. |
When the server starts or a request is made. |
| Flexibility | Low. Values are frozen into the build. | High. Can be changed without a rebuild. |
| Performance | High. Values are inlined, no server lookup needed. | Slightly lower. Requires server-side execution. |
| Ideal Use Case | Static sites, simple deployments, unchanging values. | Docker/Kubernetes, multi-environment pipelines. |
| Example | A NEXT_PUBLIC_ variable in a client component. |
A DATABASE_URL used in a Route Handler. |
In practice, most sophisticated applications end up using a hybrid approach. You'll use build-time variables for non-sensitive, public values and lean on runtime variables for all your secrets and environment-specific configuration. Mastering both is key to building scalable and maintainable Next.js applications.
Deployment and Secrets Management Best Practices
Getting your Next.js environment variables to work on your local machine is just the first step. The real test comes when you push your application to a live server. Managing your secrets securely and making sure the right variables are active in the right place—whether it's a staging preview or the final production build—is an absolutely critical part of the deployment puzzle.
Think of your local .env.local file as your private workshop. It’s where you keep your personal tools and half-finished projects. You'd never ship your entire workshop to a customer; you'd send the finished product. Deploying a Next.js app follows the same logic. You have to translate that local, private setup into a secure and repeatable process for your hosting provider.
Managing Variables on Vercel and Netlify
Fortunately, platforms like Vercel and Netlify were built with Next.js in mind, so they make this transition pretty painless. Instead of relying on .env files in production (which you should never do), you’ll use their dashboards to manage your Next.js environment variables.
Both platforms give you a simple UI to plug in your variables. The real magic here is that these secrets are encrypted and only injected into the build and runtime environments at the last possible moment. They never touch your Git repository.
You can also assign variables to different environments:
- Production: For your live, customer-facing site.
- Preview: For deployments automatically generated from pull requests.
- Development: To sync variables back down to your local machine with a CLI command.
This scoping is incredibly useful. It means you can use a test database connection for all your preview branches while the live site uses the real production key—all without changing a single line of code.
Key Takeaway: In a modern workflow, your Git repository holds the code, and your hosting provider holds the secrets. Keeping these two separate is fundamental to building secure applications.
Security Hygiene: Keeping Secrets Out of Git
Let’s be crystal clear on the golden rule: never, ever commit files containing secrets to your Git repository. This means .env.local, .env.development.local, and .env.production.local have no place in your version control history. The default Next.js .gitignore file already includes *.local for this reason, but it’s a rule so important it’s worth repeating.
So, how do you tell your teammates which variables they need to get the project running? The answer is a simple, time-tested convention: the .env.example file.
This file gets committed to your repository and acts as a template. It lists every environment variable the project needs, but with placeholder or empty values.
A solid .env.example might look like this:
Private Server-Side Variables
DATABASE_URL=
SENDGRID_API_KEY=
Public Client-Side Variables
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=
When a new developer joins, they just copy this file, rename it to .env.local, and fill in their own credentials. It's a clean, self-documenting method for managing configuration without exposing anything sensitive. This is especially vital on larger teams building complex systems. If you're interested in learning more about that, check out our guide on cloud-based application development.
Containerized Deployments with Docker
If you’re deploying your Next.js app with Docker, the core principle of separating code from configuration still applies, but the mechanics are a bit different. You should never bake .env files directly into your Docker image. Doing so creates an insecure and inflexible artifact that can't be promoted across different environments.
Instead, you inject environment variables into the container when it starts up. This powerful technique allows you to use the exact same Docker image for development, staging, and production, simply by feeding it a different set of variables each time.
You have a couple of options for doing this:
- Using
-eflags: You can pass variables one by one right in the run command:docker run -e DATABASE_URL="your-db-string" my-nextjs-app. This is fine for one or two variables. - Using an
--env-file: For anything more complex, the better approach is to store an environment's variables in a file on the host machine and pass the whole file to Docker:docker run --env-file ./production.env my-nextjs-app.
The second method keeps your startup command clean and cleanly separates your configuration from your runtime execution—a hallmark of modern DevOps practices.
Troubleshooting Common Environment Variable Issues
Even with the best setup, environment variables can sometimes be a real headache. We’ve all been there: a variable is mysteriously undefined where it shouldn’t be, or an old value just won’t go away. Let's walk through the most common snags and how to fix them fast.
The Classic "Undefined in the Browser" Problem
This is, without a doubt, the number one issue developers face. You've added API_URL to your .env.local, you've restarted the server, but process.env.API_URL is stubbornly undefined inside your React component.
Here's the secret: This isn't a bug; it's a critical security feature. To prevent you from accidentally leaking sensitive keys to the public, Next.js intentionally blocks every environment variable from the browser by default.
To make a variable accessible to your client-side code, you have to explicitly opt-in by prefixing it with NEXT_PUBLIC_.
- Symptom: You're trying to use
process.env.STRIPE_KEYinside a component, maybe one with a'use client'directive at the top. - Fix: Just rename the variable. Go into your
.envfile and changeSTRIPE_KEYtoNEXT_PUBLIC_STRIPE_KEY.
For instance, this will always be undefined in browser-run code:
// This won't work in a client component.
const apiKey = process.env.API_KEY;
The simple fix is to rename the variable in both your .env file and your code:
// in .env.local
NEXT_PUBLIC_API_KEY="pk_test_12345"
// In your component
// Now it works!
const apiKey = process.env.NEXT_PUBLIC_API_KEY;
Why Aren't My Changes Showing Up?
Another head-scratcher is when you edit an .env file, but your application keeps using the old value. You change an API endpoint, hit save, refresh the page… and nothing.
This happens because Next.js only reads your .env files once—right when the development server starts. It doesn't watch them for changes while it's running.
The solution is simple: you have to stop and restart your development server. A quick Ctrl+C in your terminal followed by npm run dev will force Next.js to load the fresh values.
Using console.log the Right Way for Debugging
When all else fails, a well-placed console.log is your best friend. But where you place it is everything.
To check server-side variables: Put a
console.log(process.env)in a Route Handler, an API Route, or an older pages-based function likegetServerSideProps. The output will show up in your terminal—the same window where you rannext dev.To check client-side variables: Use
console.log(process.env)inside a React component, preferably within auseEffecthook to ensure it runs in the browser. This output will appear in your browser's developer console.
By comparing the two logs, you'll see in an instant which variables are available on the server versus which ones have been correctly exposed to the browser with the NEXT_PUBLIC_ prefix.
Frequently Asked Questions About Next.js Environment Variables
Even when you feel like you've got a handle on the basics, a few common questions always seem to pop up when working with Next.js environment variables. Let's walk through some of the most frequent sticking points so you can get back to building.
Why Are My Environment Variables Undefined in the Browser?
This is, without a doubt, the number one stumbling block. If you're seeing undefined for a variable in a React component, the cause is almost always the same: it's missing the NEXT_PUBLIC_ prefix.
For security, Next.js intentionally keeps all environment variables on the server by default. To expose a variable to the browser, you must explicitly opt-in by prefixing its name with NEXT_PUBLIC_. So, DATABASE_URL won't work on the client, but NEXT_PUBLIC_API_ENDPOINT will. It's a simple rule, but it's the key to preventing accidental leaks of sensitive keys.
Should I Commit My .env Files to Git?
Absolutely not. You should never commit files that contain secrets—API keys, database passwords, and the like—to your repository. This is why the default Next.js .gitignore file already includes entries for .env.local, .env.development.local, and .env.production.local. These files are meant for your machine only.
A much better practice is to commit an example file, typically named
.env.example. This file lists all the required variables your project needs but leaves their values blank. It serves as a perfect template for other developers (and your future self) to set up their local environment without exposing any secrets.
How Do I Use Different Variables for Production and Development?
Next.js has a clever, built-in system for managing this. It all comes down to which file you use:
.env.development: Variables here are loaded only when you runnext dev. Perfect for development-specific settings, like pointing to a local API..env.production: These variables are loaded when you runnext buildandnext start. This is where your production API endpoints and keys should live.
And remember, .env.local is the ultimate override for your local machine. Any variable defined there will take precedence over all other files, making it ideal for your personal, non-shared keys.
Do I Need to Restart My Dev Server After Changing .env Files?
Yes, you do. This is a crucial step that catches people off guard all the time. Next.js only loads your environment variables at the very start of the process.
If you add, remove, or change a variable in any .env file, the change won't take effect until you completely stop your development server (Ctrl+C) and start it up again with next dev.
At Next.js & React.js Revolution, we're all about helping you master the tools that define the modern web. For more deep-dives, tutorials, and practical advice, be sure to check out our daily publications at https://nextjsreactjs.com.
