Environment Variables: Your First Line of Defense
Environment variables keep secrets out of code. For AI-generated applications, they're essential—because AI frequently hardcodes credentials.
What Are Environment Variables?
Environment variables are configuration values stored outside your code:
# Instead of this (in code)
const apiKey = "sk_live_abc123"# You do this (in environment)
const apiKey = process.env.API_KEY
The actual value lives in your deployment environment, not your repository.
The .env File System
File Hierarchy
.env # Shared defaults (committed, no secrets)
.env.local # Local overrides (never committed)
.env.development # Development defaults
.env.production # Production defaults (no secrets)
.env.development.local # Local dev secrets (never committed)
.env.production.local # Local prod secrets (never committed)Load Order (Next.js)
- .env.local (highest priority)
- .env.[environment].local
- .env.[environment]
- .env (lowest priority)
What Goes Where
.env (committed)
# Non-sensitive defaults
NEXT_PUBLIC_APP_NAME=MyApp
NEXT_PUBLIC_API_URL=https://api.myapp.com
LOG_LEVEL=info.env.local (never committed)
# Actual secrets for local development
DATABASE_URL=postgres://localhost:5432/myapp
STRIPE_SECRET_KEY=sk_test_abc123
OPENAI_API_KEY=sk-proj-xyz789.gitignore Configuration
Always include:
# Environment files with secrets
.env.local
.env.*.local
.env.development.local
.env.production.local# Common secret file patterns
*.pem
*.key
credentials.json
service-account.json
Platform-Specific Setup
Vercel
- Project Settings → Environment Variables
- Add variable name and value
- Select environments (Production, Preview, Development)
- Mark as "Sensitive" for secrets
Railway
- Project → Variables
- Add variable
- Automatically available to all deployments
Render
- Service → Environment
- Add Environment Variable
- Mark as secret if needed
GitHub Actions
- Repository → Settings → Secrets
- Add repository secret
- Access via
secrets.SECRET_NAME
jobs:
deploy:
runs-on: ubuntu-latest
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}Variable Naming Conventions
Server-Side Only
# Use plain names - these stay on the server
DATABASE_URL=...
STRIPE_SECRET_KEY=...
JWT_SECRET=...Client-Side Exposure
# NEXT_PUBLIC_ prefix exposes to browser
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=...Naming Best Practices
# Good - Clear and descriptive
DATABASE_URL
STRIPE_SECRET_KEY
SUPABASE_SERVICE_ROLE_KEY# Bad - Unclear
DB
KEY
SECRET
Security Rules
Rule 1: Never Commit Secrets
# Check for accidentally committed secrets
git log -p grep -i "api_key\ secret\
password"# If found, remove from history
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch .env.local' \
--prune-empty --tag-name-filter cat -- --all
Rule 2: Rotate Compromised Secrets
If a secret was ever in your repo:
- Generate a new secret immediately
- Update all environments
- Invalidate the old secret
- Check for unauthorized usage
Rule 3: Different Secrets Per Environment
# Production (Vercel Environment Variables)
STRIPE_SECRET_KEY=sk_live_real_key# Development (.env.local)
STRIPE_SECRET_KEY=sk_test_test_key
Rule 4: Minimal Exposure
// WRONG - Exposing secret to client
NEXT_PUBLIC_DATABASE_URL=postgres://user:pass@host/db// RIGHT - Server only
DATABASE_URL=postgres://user:pass@host/db
Common Environment Variables
Database
DATABASE_URL=postgres://user:password@host:5432/database
MONGODB_URI=mongodb+srv://user:password@cluster/databaseAuthentication
NEXTAUTH_SECRET=random-32-char-string
NEXTAUTH_URL=https://myapp.com
JWT_SECRET=another-random-stringThird-Party Services
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
OPENAI_API_KEY=sk-...
RESEND_API_KEY=re_...Supabase
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc... # Safe for client
SUPABASE_SERVICE_ROLE_KEY=eyJhbGc... # Server only!Validation at Startup
Catch missing variables early:
// lib/env.ts
const requiredEnvVars = [
'DATABASE_URL',
'STRIPE_SECRET_KEY',
'NEXTAUTH_SECRET',
]for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(Missing required environment variable: \${envVar})
}
}
export const env = {
databaseUrl: process.env.DATABASE_URL!,
stripeSecretKey: process.env.STRIPE_SECRET_KEY!,
nextAuthSecret: process.env.NEXTAUTH_SECRET!,
}
Type-Safe Environment Variables
With Zod:
// lib/env.ts
import { z } from 'zod'const envSchema = z.object({
DATABASE_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
NEXTAUTH_SECRET: z.string().min(32),
NODE_ENV: z.enum(['development', 'production', 'test']),
})
export const env = envSchema.parse(process.env)
Environment Variable Checklist
SETUP
=====
[ ] .env.local in .gitignore
[ ] .env.*.local in .gitignore
[ ] No secrets in committed .env files
[ ] All secrets in platform environment variablesDEVELOPMENT
===========
[ ] .env.local has all needed variables
[ ] Using test keys (not production)
[ ] Database points to dev environment
PRODUCTION
==========
[ ] All secrets set in deployment platform
[ ] Production keys are different from dev
[ ] Sensitive variables marked as secret
[ ] NEXTAUTH_URL set to production domain
MAINTENANCE
===========
[ ] Secrets rotated regularly
[ ] Unused variables removed
[ ] Access to secrets limited to needed team members
The Bottom Line
Environment variables are simple but critical. Keep secrets out of code, never commit .env.local, and use different values for each environment.
One leaked secret can compromise everything. Environment variables prevent that leak.