The Mental Model
Not all keys are created equal.
Think of your app like a concert venue. Some keys let people in the front door. Others open the vault backstage. Treat them differently.
OK to show. Identifies you but can't do damage. Like NEXT_PUBLIC_STRIPE_KEY.
Never share. Lets anyone charge cards, send emails, access your database. Guard it with your life.
Where keys live at runtime. Never in your code. Never in Git. Only on the server.
Prevents your vault file from ever being committed to Git.
# Public keys (safe in browser)
NEXT_PUBLIC_STRIPE_KEY=pk_test_51N8x...
# Secret keys (server only!)
STRIPE_SECRET_KEY=sk_test_51N8x...
OPENAI_API_KEY=sk-proj-abc123...
RESEND_API_KEY=re_1a2b3c...
# Database
DATABASE_URL=postgresql://user:pass@...
The .env File
Where your secrets live.
Next.js loads environment variables from .env files automatically. But not all .env files are equal.
The hierarchy matters. More specific files override general ones. And crucially, .env.local is automatically added to .gitignore by Next.js.
.env
All environments. Committed to Git. No secrets here.
.env.local
Local only. Gitignored. This is where your secrets go.
.env.production
Production overrides. Use for non-secret production config.
.env.development
Dev overrides. Useful for local-only defaults.
# ============================================
# NEXT_PUBLIC_* variables are exposed to the
# browser. Only use for non-sensitive data.
# ============================================
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# ============================================
# Server-only variables. NEVER prefix with
# NEXT_PUBLIC_ or they leak to the client.
# ============================================
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
OPENAI_API_KEY=sk-proj-...
RESEND_API_KEY=re_...
DATABASE_URL=postgresql://...
Priority: .env.local > .env.development > .env
Test Mode vs. Live Mode
Test Mode
SafeA sandbox where nothing is real. No actual charges. No real emails sent. Perfect for development.
// Stripe test keys start with:
pk_test_... // publishable
sk_test_... // secret
// Test card number:
4242 4242 4242 4242
Any future date, any CVC
Live Mode
Real MoneyThe real deal. Actual charges on real credit cards. Only use in production. Never in development.
// Stripe live keys start with:
pk_live_... // publishable
sk_live_... // secret
// Set in production only:
STRIPE_SECRET_KEY=sk_live_...
Set via Vercel dashboard, not .env
The Danger Scale
The .gitignore Shield
The file that keeps secrets out of Git.
.gitignore tells Git which files to pretend don't exist. If a file matches a pattern in .gitignore, it will never be committed.
Next.js adds .env*.local to .gitignore by default. But you should always double-check.
If you accidentally commit a key…
GitHub will email you a security alert within seconds. Bots scan every public commit for patterns like sk_live, sk-proj, and password=.
Deleting the file isn't enough
The key is still in your Git history. You'd need to rotate the key immediately and rewrite history with git filter-branch.
Prevention is the only cure
Always verify .gitignore before your first commit. Run git status and confirm .env files don't appear.
# dependencies
/node_modules
# next.js
/.next/
/out/
# production
/build
# environment variables
.env*.local
# misc
.DS_Store
*.tsbuildinfo
next-env.d.ts
✓ .env*.local is gitignored by default in Next.js
Always verify before your first push
Common Patterns
✗ Security Traps
"I'll just paste the key inline"
Hardcoded keys end up in Git. Even if you delete them later, they're in the history forever.
"I committed .env but it's fine"
GitHub bots scan every push. Your key will be found and exploited within minutes.
"I'll use NEXT_PUBLIC_ for my secret key"
NEXT_PUBLIC_ vars are bundled into client-side JavaScript. Anyone can read them in DevTools.
"I use live keys in development"
One test gone wrong and you charge a real card. Always use test keys locally.
✓ The Vault
Use process.env for all keys
Keys live in .env.local, loaded at runtime. Never in source code.
Verify .gitignore first
Before your first commit, run git status and confirm .env files aren't tracked.
NEXT_PUBLIC_ only for non-secrets
Publishable keys, app URLs, feature flags. Never secret keys or tokens.
Set production keys in Vercel dashboard
Go to Settings → Environment Variables. Keys are encrypted and injected at build time.
The Exercises
Lock down your secrets.
The Vault Setup
Create & Protect
- →Create a
.env.localfile in your project root - →Add a test API key:
MY_SECRET=hello123 - →Verify
.gitignoreincludes.env*.local
The Access Pattern
Server vs. Client
- →Access the key in a server component via
process.env.MY_SECRET - →Try accessing it in a client component
- →Confirm it's
undefinedon the client
The Audit
Check Your History
- →Run
git log --all -p | grep -i "key\|secret\|password" - →Check if any real credentials appear in your Git history
- →If found: rotate the key immediately, then clean history
📋 Quick Reference
Access a Secret
process.env.STRIPE_SECRET_KEYClient-Safe Variable
NEXT_PUBLIC_APP_URL=...Audit for Leaks
git log -p | grep -i secret