| name | infra-env |
| description | Environment variable conventions and security practices for Next.js projects. This skill should be used when setting up environment configuration, managing secrets, or establishing security patterns for a new project. |
Environment Variables Skill
Conventions and security practices for managing environment variables in Next.js projects.
When to Use This Skill
- Setting up a new project's environment configuration
- Adding new environment variables
- Reviewing security of environment handling
- Creating
.env.example templates
- Troubleshooting environment issues
Core Principles
- Never commit secrets -
.env* files with real values stay out of git
- Document with examples -
.env.example shows structure without values
- Prefix client-safe vars - Only
NEXT_PUBLIC_* reaches the browser
- Validate at startup - Fail fast if required vars are missing
- Use typed configuration - Type-safe access to environment
File Hierarchy
| File | Purpose | Git? |
|---|
.env | Default values (shared) | Optional |
.env.local | Local overrides (secrets) | Never |
.env.development | Dev-specific defaults | Optional |
.env.production | Prod defaults (no secrets) | Optional |
.env.example | Template for developers | Always |
Load order (later overrides earlier):
.env
.env.local
.env.development / .env.production (based on NODE_ENV)
.env.development.local / .env.production.local
Standard .env.example
Create .env.example as documentation:
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_APP_NAME=MyApp
DATABASE_URL=postgres://postgres:postgres@localhost:5432/myapp
BETTER_AUTH_SECRET=your-secret-key-min-32-characters-here
BETTER_AUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
Naming Conventions
Server-Only Variables (Default)
DATABASE_URL=...
BETTER_AUTH_SECRET=...
GOOGLE_CLIENT_SECRET=...
RESEND_API_KEY=...
OPENAI_API_KEY=...
Client-Accessible Variables
Must start with NEXT_PUBLIC_:
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_POSTHOG_KEY=phc_xxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_xxx
Naming Style
- Use
SCREAMING_SNAKE_CASE
- Be descriptive:
DATABASE_URL not DB
- Group by service:
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
- Prefix third-party:
RESEND_API_KEY, STRIPE_SECRET_KEY
Type-Safe Environment
Environment Validation
Create src/lib/env.ts:
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
BETTER_AUTH_SECRET: z.string().min(32),
BETTER_AUTH_URL: z.string().url(),
GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(),
GITHUB_CLIENT_ID: z.string().optional(),
GITHUB_CLIENT_SECRET: z.string().optional(),
NEXT_PUBLIC_APP_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error('❌ Invalid environment variables:');
console.error(parsed.error.flatten().fieldErrors);
throw new Error('Invalid environment variables');
}
export const env = parsed.data;
Usage
import { env } from '@/lib/env';
const dbUrl = env.DATABASE_URL;
const isProduction = env.NODE_ENV === 'production';
Client Environment
For client-side validation, create src/lib/env-client.ts:
import { z } from 'zod';
const clientEnvSchema = z.object({
NEXT_PUBLIC_APP_URL: z.string().url(),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
});
export const clientEnv = clientEnvSchema.parse({
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
});
Security Practices
Never Do
API_KEY=sk_live_real_key_here
NEXT_PUBLIC_DATABASE_URL=postgres://...
Always Do
API_KEY=your-api-key-here
DATABASE_URL=postgres://...
NEXT_PUBLIC_APP_URL=http://localhost:3000
.gitignore
Ensure these are in .gitignore:
# Environment files with secrets
.env.local
.env.*.local
.env.development.local
.env.production.local
# Keep .env.example in git
!.env.example
Secret Generation
openssl rand -base64 32
openssl rand -hex 32
uuidgen
Per-Environment Configuration
Development
NODE_ENV=development
NEXT_PUBLIC_APP_URL=http://localhost:3000
Production
NODE_ENV=production
DATABASE_URL=postgres://prod-user:prod-pass@prod-host:5432/prod-db
BETTER_AUTH_SECRET=production-secret-here
NEXT_PUBLIC_APP_URL=https://myapp.com
Testing
NODE_ENV=test
DATABASE_URL=postgres://postgres:postgres@localhost:5432/myapp_test
Platform-Specific Setup
Local Development
- Copy template:
cp .env.example .env.local
- Fill in values for local services
- Start Docker services:
docker compose up -d
- Run app:
npm run dev
Railway
railway variables --set DATABASE_URL=postgres://...
railway variables --set BETTER_AUTH_SECRET=...
Vercel
vercel env add DATABASE_URL production
vercel env add BETTER_AUTH_SECRET production
AWS ECS
Use AWS Secrets Manager or Parameter Store:
aws secretsmanager create-secret \
--name myapp/production/database-url \
--secret-string "postgres://..."
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:region:account:secret:myapp/production/database-url"
}
]
Debugging Environment Issues
Check What's Loaded
console.log('DATABASE_URL exists:', !!process.env.DATABASE_URL);
console.log('NODE_ENV:', process.env.NODE_ENV);
Common Issues
Variable undefined at runtime:
- Check file is named correctly (
.env.local not .env.local.txt)
- Restart dev server after adding new variables
- Verify variable name matches exactly (case-sensitive)
Client can't access variable:
- Must have
NEXT_PUBLIC_ prefix
- Must rebuild after adding (for production builds)
Wrong value used:
- Check load order (
.env.local overrides .env)
- Clear
.next cache: rm -rf .next
Works locally, fails in production:
- Variables set in deployment platform?
- Names match exactly?
- No extra spaces or quotes in values?
Checklist for New Projects
Quick Reference
| Variable Type | Prefix | Accessible In |
|---|
| Server secret | None | Server only |
| Public config | NEXT_PUBLIC_ | Server + Client |
| File | Committed? | Contains Secrets? |
|---|
.env.example | Yes | No (placeholders) |
.env | Optional | No |
.env.local | No | Yes |