Environment Variables and Secrets Management in Next.js
Environment variables have come up repeatedly throughout this module — a database connection URL, OAuth provider credentials, cloud storage keys — always with the instruction to store them as environment variables rather than hardcoding them. This lesson finally covers environment variables in Next.js properly and completely: the different .env file types Next.js recognizes, the critical distinction between server-only and browser-exposed variables (controlled by the NEXT_PUBLIC prefix), and best practices for keeping genuinely sensitive secrets secure across development and production.
Learning Objectives
- Understand the different .env file types Next.js supports and when each is loaded.
- Explain the critical difference between server-only and NEXT_PUBLIC-prefixed environment variables.
- Correctly decide whether a given value should be a public or server-only environment variable.
- Keep sensitive .env files out of version control while still sharing configuration structure with a team.
- Manage environment variables securely across different deployment environments.
Core Definitions
- Environment variable: A configuration value available to your application at runtime, defined outside your source code, commonly used for values that differ across environments or that must remain secret.
- .env.local: The primary, git-ignored environment file for local development secrets, taking precedence over other .env files.
- NEXT_PUBLIC_ prefix: A required naming convention in Next.js that exposes an environment variable's value to browser-side (client) code; variables without this prefix remain server-only.
- Secret: A sensitive value (like an API key or database password) that must never be exposed to the browser or committed to version control.
- .env.example: A conventional, committed file listing the names of required environment variables with placeholder values, documenting configuration needs without exposing real secrets.
Detailed Explanation
Next.js recognizes several .env file variants, each with a specific purpose and precedence. .env is a base file, typically committed with non-sensitive defaults. .env.local overrides it and is specifically meant for local secrets — critically, Next.js automatically adds .env.local to a project's default .gitignore-relevant patterns, meaning it should never actually be committed to version control. .env.development and .env.production let you define values specific to each respective environment, useful for things like pointing to a local database URL during development versus a real production one automatically based on which mode your app is running in.
The single most important, easy-to-get-wrong concept is the NEXT_PUBLIC_ prefix. By default, environment variables in Next.js are server-only — accessible in Server Components, Server Actions, Route Handlers, and other server-side code, but completely inaccessible in code that runs in the browser (Client Components). If you need a specific value available in client-side JavaScript — say, a public API endpoint URL, or a publishable (non-secret) API key for a third-party service like Stripe's client-side library — you must explicitly prefix that variable's name with NEXT_PUBLIC_ (e.g., NEXT_PUBLIC_API_URL). Doing so tells Next.js to actually bake that variable's value directly into the browser-side JavaScript bundle at build time, making it genuinely, permanently visible to anyone who inspects your site's client-side code.
This has a critical security implication: NEXT_PUBLIC_-prefixed variables should NEVER be used for genuine secrets — a database password, a private API key, a JWT signing secret. Since these values become embedded directly in publicly downloadable browser JavaScript, prefixing a secret with NEXT_PUBLIC_ effectively publishes that secret to the entire world, completely defeating its purpose. Genuine secrets must always remain unprefixed, server-only variables, accessed exclusively from server-side code (Server Components, Server Actions, Route Handlers) that never ships to the browser.
For team collaboration, since .env.local (containing real secrets) is correctly excluded from version control, a common and recommended practice is committing a .env.example file instead — listing every environment variable your application needs, with placeholder or empty values, serving as living documentation of your app's configuration requirements without exposing any actual sensitive values. A new team member clones the repository, copies .env.example to their own .env.local, and fills in their own real values (development database credentials, their own API keys) without ever needing access to production secrets.
For production deployments, actual secret values are typically configured directly through your hosting provider's dashboard or CLI (as encrypted environment variables specific to that deployment), rather than through any file at all — keeping production secrets entirely separate from your codebase and its version control history, managed instead through your hosting platform's own secure configuration mechanisms.
Server-Only vs NEXT_PUBLIC Environment Variables
{"heading":"Server-Only vs NEXT_PUBLIC Environment Variables","description":"Visualize the critical server-only vs NEXT_PUBLIC distinction:\n\nDATABASE_URL=postgresql://... ← NO prefix: SERVER-ONLY, never reaches the browser\nNEXT_PUBLIC_API_URL=https://api... ← NEXT_PUBLIC_ prefix: baked into the BROWSER bundle, publicly visible\n\n[Server Component reads DATABASE_URL] --> Works fine, stays server-side, never exposed\n[Client Component reads DATABASE_URL] --> undefined! Server-only vars aren't available client-side\n[Client Component reads NEXT_PUBLIC_API_URL] --> Works, value is baked into the public JS bundle\n\nNEVER do this: NEXT_PUBLIC_DATABASE_PASSWORD=... ← This would PUBLISH the secret to every visitor's browser!"}
Next.js Practical Example
// .env.local — real secrets, NEVER committed to git
DATABASE_URL=postgresql://user:realpassword@host:5432/mydb
AUTH_SECRET=a-real-random-secret-value
GITHUB_SECRET=real-github-oauth-secret
NEXT_PUBLIC_API_URL=https://api.example.com
// .env.example — committed, documents structure with no real values
DATABASE_URL=
AUTH_SECRET=
GITHUB_SECRET=
NEXT_PUBLIC_API_URL=
// lib/db.ts — reading a server-only variable (Server Component/Action/Route Handler only)
const dbUrl = process.env.DATABASE_URL; // fine — this code never runs in the browser
// components/ApiStatus.tsx — a Client Component reading a NEXT_PUBLIC variable
'use client';
export default function ApiStatus() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL; // correctly exposed, non-sensitive
return <p>Connected to: {apiUrl}</p>;
}
.env.local holds the real, sensitive values for this developer's local setup — DATABASE_URL and AUTH_SECRET are genuine secrets and correctly have no NEXT_PUBLIC_ prefix, keeping them accessible only to server-side code. NEXT_PUBLIC_API_URL, by contrast, is a non-sensitive value (just a public API endpoint) intentionally exposed to the browser via its required prefix. .env.example mirrors the same variable names with empty placeholder values, safely committed to version control as documentation for the team, without leaking any of the actual secret values from .env.local. ApiStatus, a Client Component, correctly reads only the NEXT_PUBLIC_-prefixed variable — attempting to read process.env.DATABASE_URL from this same Client Component would return undefined, since server-only variables are never included in the browser-side JavaScript bundle.
How Teams Manage Secrets Across Environments
- Startups and small teams commonly commit a thorough .env.example file to their repository specifically to speed up onboarding new engineers, who can get a working local environment running within minutes.
- Enterprises with strict compliance requirements manage production secrets exclusively through dedicated secrets management services (like AWS Secrets Manager or HashiCorp Vault) rather than even their hosting provider's basic environment variable dashboard, for additional auditing and rotation capabilities.
- SaaS platforms integrating payment providers like Stripe carefully distinguish between a publishable key (safely NEXT_PUBLIC_-prefixed, used in client-side checkout widgets) and a secret key (server-only, used for actual charge processing), since confusing the two would be a serious security vulnerability.
- Open-source projects accepting community contributions rely heavily on .env.example files, since contributors need to know exactly which environment variables are required to run the project locally without ever having access to the maintainers' actual production secrets.
- Multi-environment CI/CD pipelines (development, staging, production) commonly configure separate, appropriately scoped environment variables per environment directly through their hosting or CI provider's dashboard, rather than relying on committed .env files for anything beyond local development.
Common Mistakes to Avoid
- Prefixing a genuine secret (database password, private API key) with NEXT_PUBLIC_, accidentally exposing it in the public browser JavaScript bundle.
- Committing a .env.local file containing real secrets to version control, exposing them in the repository's history.
- Forgetting to add a needed NEXT_PUBLIC_ prefix for a genuinely non-sensitive value that a Client Component actually needs, causing it to read as undefined in the browser.
- Not maintaining a .env.example file, making it harder for new team members to know exactly which environment variables are required to run the project.
- Assuming all environment variables are automatically available in Client Components, without understanding the server-only default behavior.
Interview Notes
- Next.js recognizes several .env file variants (.env, .env.local, .env.development, .env.production), each with different precedence and purpose.
- .env.local is for local secrets and should never be committed to version control.
- Environment variables are server-only by default; the NEXT_PUBLIC_ prefix is required to expose a variable's value to browser-side code.
- NEXT_PUBLIC_-prefixed values are baked directly into the public JavaScript bundle and must never be used for genuine secrets.
- A committed .env.example file documents required environment variables with placeholder values, without exposing real secrets.
Key Takeaways
- Understanding the NEXT_PUBLIC_ prefix correctly is one of the most consequential, easy-to-get-wrong security decisions in a Next.js application.
- The various .env file types give fine-grained control over configuration across local development and different deployment environments.
- A .env.example file is a small, low-effort practice with meaningful benefits for team onboarding and configuration documentation.
- Production secrets ultimately live outside your codebase entirely, managed through your hosting provider's or a dedicated secrets service's secure configuration mechanisms.
Summary
Next.js recognizes several .env file variants — .env for shared defaults, .env.local for local secrets and overrides (never committed to version control), and .env.development/.env.production for environment-specific values. The most critical concept is the NEXT_PUBLIC_ prefix: environment variables are server-only by default, accessible only in Server Components, Server Actions, and Route Handlers, while a variable explicitly prefixed with NEXT_PUBLIC_ has its value baked directly into the public, browser-downloadable JavaScript bundle at build time. This means genuine secrets — database passwords, private API keys — must never use the NEXT_PUBLIC_ prefix, since doing so would effectively publish that secret to the entire world. A committed .env.example file, listing required variable names with placeholder values, documents an application's configuration needs for team members without exposing any real secrets, while actual production secrets are typically managed directly through a hosting provider's dashboard or a dedicated secrets management service, kept entirely separate from the project's version-controlled codebase.