Manage environment variables and configure Next.js for different environments.
# .env.local
DATABASE_URL=postgresql://user:password@localhost/db
API_SECRET=my-secret-key# .env.production.local
DATABASE_URL=postgresql://user:password@prod/db
API_SECRET=production-secret# .env.development
NEXT_PUBLIC_API_URL=http://localhost:4000
LOG_LEVEL=debug# .env (committed — no secrets)
NEXT_PUBLIC_APP_NAME=My App
NEXT_PUBLIC_SUPPORT_EMAIL=support@example.com# Priority (highest to lowest):
# 1. .env.{NODE_ENV}.local (e.g. .env.development.local)
# 2. .env.local (skipped in test environment)
# 3. .env.{NODE_ENV} (e.g. .env.development)
# 4. .env (shared defaults)
# The FIRST file that defines a variable wins — later files cannot override it.# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_APP_NAME=My Appfunction Component() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
return <p>API: {apiUrl}</p>;
}// process.env.NEXT_PUBLIC_API_URL becomes a string literal in the bundle
const url = process.env.NEXT_PUBLIC_API_URL; // "https://api.example.com"NEXT_PUBLIC_STRIPE_KEY=pk_live_abc123
NEXT_PUBLIC_GA_ID=G-XXXXXXX# .env.local
DATABASE_URL=postgresql://...
API_KEY=secret-keyexport async function GET() {
const apiKey = process.env.API_KEY;
// Safe: runs on server only
}"use server";
export async function deleteUser(id: string) {
await db.user.delete({ where: { id } });
// process.env.DATABASE_URL is safe here
}"use client";
// process.env.API_KEY is undefined — not leaked to browser// next.config.js
module.exports = {
env: {
APP_VERSION: process.env.npm_package_version, // inlined at build time
FEATURE_FLAG: process.env.FEATURE_FLAG
}
};// next.config.js
module.exports = {
serverRuntimeConfig: {
mySecret: process.env.MY_SECRET // server-only, not in browser
},
publicRuntimeConfig: {
apiUrl: process.env.NEXT_PUBLIC_API_URL // shared with client
}
};// lib/env.ts — imported in app/layout.tsx
const required = ["DATABASE_URL", "AUTH_SECRET"];
for (const key of required) {
if (!process.env[key]) throw new Error(`Missing env var: ${key}`);
}import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: { DATABASE_URL: z.string().url() },
client: { NEXT_PUBLIC_API_URL: z.string().url() },
runtimeEnv: process.env
});# After editing .env.local or adding new variables:
npm run dev # restart required for new vars to take effect# .gitignore
.env.local
.env.development.local
.env.test.local
.env.production.local# .env.example (safe to commit)
DATABASE_URL=postgresql://user:password@localhost/db
NEXT_PUBLIC_API_URL=https://api.example.com
AUTH_SECRET="use client";
// ❌ DATABASE_URL is undefined in the browser — not leaked, just missing
const url = process.env.DATABASE_URL;# Safe to expose
NEXT_PUBLIC_STRIPE_KEY=pk_live_abc123
NEXT_PUBLIC_GA_ID=G-XXXXXXX
# Never prefix with NEXT_PUBLIC_
STRIPE_SECRET=sk_live_secret
DATABASE_URL=postgres://prod-secret# Vercel CLI
vercel env add DATABASE_URL production
# GitHub Actions secret
# Settings > Secrets > Actions > New repository secretPrefix the variable name with NEXT_PUBLIC_ (e.g., NEXT_PUBLIC_API_URL) and Next.js will inline its value at build time, making it available in both client and server code. Variables without this prefix are stripped from the client bundle entirely.
.env is for default values shared across environments and can be committed to version control, while .env.local overrides those values locally and should never be committed. Next.js loads both files, but .env.local always takes precedence.
Make sure the variable is defined in your .env.local or .env file and that you are not accidentally prefixing it with NEXT_PUBLIC_ while only expecting server-side access. Also confirm the dev server was restarted after adding or changing the variable, since Next.js does not hot-reload env files.
Yes — use the env key in next.config.js to explicitly expose specific variables to the client, or use serverRuntimeConfig for server-only values and publicRuntimeConfig for values shared with the client at runtime rather than build time.
Next.js loads environment files in priority order from highest to lowest: .env.{NODE_ENV}.local, then .env.local (skipped in test), then .env.{NODE_ENV}, then .env. The first file that defines a variable wins — lower-priority files cannot override it. Use .env.local for secrets that should never be committed, and .env for shared defaults.