Back to Knowledge
Updated Sep 2026

Environment Variables

Secret notes for your app. API keys, database passwords, config: everything that changes between your laptop and production, and everything that must never land in git.

1What Are Environment Variables?

An environment variable

Environment Variable

A secret value stored outside your code, like API keys or passwords. Keeps sensitive info out of your codebase.

"Like a sticky note with the WiFi password — you know it, but you don't write it on the wall."

is a named value your app reads at runtime instead of hardcoding it. Your code says process.env.DATABASE_URL; the environment fills in the real value. Your laptop points at a dev database, production points at the real one, same code.

Think of it like a hotel safe: the code knows which safe to open, but the combination is set per room.

Why this matters

Bots scan public GitHub for leaked API keys

API Key

A unique code that identifies you when using an API. It's how services know who's making requests (and who to bill).

"Like a VIP pass. It proves you're allowed in and tracks your usage."

around the clock. A key pushed by accident can be abused within minutes and run up a real bill. Env vars keep secrets out of your code.

2The .env Files

Create .env.local in your project root (next to package.json, not inside src/). One KEY=value per line. Next.js loads it automatically for next dev and next build.

.env.local

# Database
DATABASE_URL="postgresql://user:pass@host/db"

# Auth (Auth.js v5 naming)
AUTH_SECRET="generated-by-npx-auth-secret"
AUTH_GITHUB_ID="Ov23li..."
AUTH_GITHUB_SECRET="..."

# Stripe
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."

# Public: bundled into browser JavaScript
NEXT_PUBLIC_APP_URL="http://localhost:3000"

Which file wins? Next.js checks, in order, and stops at the first hit:

  1. Real environment (process.env, e.g. set by Vercel)
  2. .env.development.local / .env.production.local
  3. .env.local (skipped when running tests)
  4. .env.development / .env.production
  5. .env

Critical: keep them out of git

create-next-app already ignores .env* files. Check yours before the first commit, and commit a .env.example with names but no values so teammates (and Claude) know what's needed.

.gitignore

.env*
!.env.example

3Server-Only vs NEXT_PUBLIC_

Private (server only): the default

Plain names like DATABASE_URL exist only on the server: Server Components

Server Component

A React component that runs ONLY on the server. Can directly access databases, fetch data, and keeps secrets safe. The default in Next.js App Router.

"Like the kitchen in a restaurant. Customers never see it, but that's where the magic happens."

, route handlers, Server Actions, proxy.ts. They never reach the browser. This is where every secret belongs.

app/api/stats/route.ts

const db = process.env.DATABASE_URL; // works on the server
// in a "use client" component this is undefined

Public (NEXT_PUBLIC_): baked into the bundle

Anything prefixed NEXT_PUBLIC_ is copied into your JavaScript at build time and shipped to every visitor. Anyone can open DevTools and read it. It's also frozen at build: change the value in Vercel and you must redeploy to see it.

Anywhere, including the browser

const url = process.env.NEXT_PUBLIC_APP_URL;

Fine to publish

Site URL, Stripe publishable key, analytics IDs, public feature flags.

Never NEXT_PUBLIC_

Database URLs, OAuth secrets, Stripe secret keys, AI provider keys, AUTH_SECRET.

Classic AI-assistant move: "the variable is undefined in the browser, so I added NEXT_PUBLIC_." If that variable is a secret, you just published it. The right fix is to move the code that needs it to the server.

4Local vs Vercel: Two Separate Worlds

Local development

  • • Lives in .env.local on your machine
  • • Test keys, dev database
  • • Restart npm run dev after edits

Vercel

  • • Project → Settings → Environment Variables
  • • Scoped to Production, Preview, and/or Development
  • • Applied on the next deployment

The Vercel CLI

CLI (Command Line Interface)

A program you interact with by typing commands in the terminal, rather than clicking buttons.

"Like texting vs. video calling. Faster, no frills, straight to the point."

keeps the two in sync so you're not copy-pasting secrets between browser tabs. Link the folder to your project once, then:

Terminal

vercel link                                 # once per project folder

# Vercel → your laptop
vercel env pull                             # writes .env.local (Development values)
vercel env pull --environment=preview       # grab Preview values instead

# your laptop → Vercel
vercel env add STRIPE_SECRET_KEY production # prompts for the value
vercel env update STRIPE_SECRET_KEY production
vercel env ls production                    # list what's set
vercel env rm OLD_KEY production

# run a command with Vercel's vars, no file written
vercel env run -- next dev
vercel env run -e production -- next build

Sensitive by default

vercel env add stores Production and Preview values as sensitive: usable by builds and functions, but nobody can view them again in the dashboard. Keep your own copy in a password manager. Development values can't be sensitive, so add those in a separate command.

Don't echo secrets

echo value | vercel env add ... works but saves the secret in your shell history. Let the prompt ask, or pipe from a file: vercel env add NAME production < key.txt.

5OIDC: The Secret You Don't Have to Store

Some Vercel services skip long-lived keys entirely. On Vercel, your functions get a short-lived VERCEL_OIDC_TOKEN automatically, and services like the AI Gateway

AI Gateway

A single endpoint that sits between your app and many AI providers, handling keys, routing, fallbacks, budgets, and usage tracking. With Vercel AI Gateway you pass a plain `"provider/model"` string like `'anthropic/claude-sonnet-5'` to the AI SDK and authenticate with `AI_GATEWAY_API_KEY` (or OIDC on Vercel).

"Like a travel agent who books any airline for you. One contact, one bill, and they rebook you if a flight is cancelled."

, Blob, and Sandbox accept it. Nothing to copy, nothing to leak, nothing to rotate.

Locally, vercel env pull writes a development OIDC token into .env.local for you. It expires, so if AI Gateway calls start failing with auth errors on your laptop, pull again.

Rule of thumb: if a Vercel service offers OIDC, prefer it over pasting an API key. Otherwise fall back to a key like AI_GATEWAY_API_KEY. More in AI SDK & Gateway.

6Naming Conventions That Matter

Some libraries don't just read env vars, they guess the names. Auth.js v5 auto-loads AUTH_{PROVIDER}_ID and AUTH_{PROVIDER}_SECRET. Name it GITHUB_ID (the old v4 style) and GitHub login ships with client_id=undefined, no error anywhere. The full story is in The OAuth Trap.

VariableWhat it's for
DATABASE_URLPostgres connection string (e.g. Neon)
AUTH_SECRETAuth.js session encryption. Generate with npx auth secret
AUTH_GITHUB_ID / AUTH_GITHUB_SECRETGitHub OAuth app. Auth.js v5 reads these automatically
AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRETGoogle OAuth client
STRIPE_SECRET_KEYStripe API secret (sk_test_ locally, sk_live_ in production)
STRIPE_WEBHOOK_SECRETStripe webhook signature verification
AI_GATEWAY_API_KEYVercel AI Gateway, when you're not using OIDC
NEXT_PUBLIC_APP_URLYour app's public URL
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYStripe publishable key (pk_...), safe for the browser

Yellow = public (browser-visible). Cyan = server-only secret.

7Fail Fast: Validate at Startup

A missing variable should break the build, not a user's checkout. A tiny Zod schema turns "undefined somewhere at 2am" into a clear error on deploy.

lib/env.ts

import "server-only";
import { z } from "zod";

const schema = z.object({
  DATABASE_URL: z.string().min(1),
  AUTH_SECRET: z.string().min(1),
  AUTH_GITHUB_ID: z.string().min(1),
  AUTH_GITHUB_SECRET: z.string().min(1),
  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
});

export const env = schema.parse(process.env);

Import env from server code instead of reading process.env directly, and you also get autocomplete and types for free.

8Keep Secrets Away From Your AI, Too

Coding agents read files to understand your project, and that can include .env.local. Anything they read can end up in a transcript, a log, or a prompt. Claude Code

Claude Code

Anthropic's agentic coding tool. It lives in your terminal (and in VS Code, JetBrains, and on the web), reads your codebase, edits files, runs commands, and ships code. Install with the native installer (`curl -fsSL https://claude.ai/install.sh | bash` on macOS/Linux, `irm https://claude.ai/install.ps1 | iex` on Windows); it needs a Pro, Max, Team, Enterprise, or Console account.

"Like having a senior developer living in your terminal, ready to help 24/7."

lets you deny it outright:

~/.claude/settings.json (or .claude/settings.json)

{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)"
    ]
  }
}

Give Claude .env.example instead: it learns the names without seeing the values. And never paste a real key into a chat, even "just to debug." See Prompt Injection & Security for why.

9Common Traps

"I committed my .env file!"

The secret is in git history forever, even after you delete the file. If the repo is (or ever becomes) public, assume it's already been scraped.

Fix: Rotate every exposed key first (new key in the provider dashboard, update Vercel, redeploy). Then fix .gitignore. Scrubbing history with git-filter-repo is optional cleanup, not the fix.

"process.env.X is undefined"

Typo, the file isn't in the project root, the dev server wasn't restarted, or you're reading a server-only variable in a Client Component.

Fix: Check spelling, restart npm run dev, and move secret-reading code to the server rather than adding NEXT_PUBLIC_.

"Works locally but not in production"

The variable exists in .env.local but not in Vercel, or only for Development/Preview, or you didn't redeploy.

Fix: Run vercel env ls production, add what's missing, redeploy.

Changed a NEXT_PUBLIC_ value and nothing happened

Public variables are inlined at build time.

Fix: Redeploy (a fresh build) after changing any NEXT_PUBLIC_ value.

Test keys in production (or live keys locally)

Stripe sk_test_ keys don't move real money; sk_live_ keys on your laptop can.

Fix: Live keys only in Vercel Production. Test keys in Development and Preview.

v4-style auth variable names

NEXTAUTH_SECRET, GITHUB_ID on an Auth.js v5 app.

Fix: Rename to AUTH_SECRET, AUTH_GITHUB_ID, AUTH_GITHUB_SECRET in every environment.

Secrets sorted?

Ship it, then run the pre-launch checklist before real users show up.