Security audit workflow - vulnerability scan → verification
npx skills add aussiegingersnap/cursor-skills --skill "infra-env"
Install specific skill from multi-skill repository
# 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.
# SKILL.md
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.exampletemplates - Troubleshooting environment issues
Core Principles
- Never commit secrets -
.env*files with real values stay out of git - Document with examples -
.env.exampleshows 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):
1. .env
2. .env.local
3. .env.development / .env.production (based on NODE_ENV)
4. .env.development.local / .env.production.local
Standard .env.example
Create .env.example as documentation:
# =============================================================================
# ENVIRONMENT CONFIGURATION
# =============================================================================
# Copy this file to .env.local and fill in the values
# NEVER commit .env.local to git
# =============================================================================
# -----------------------------------------------------------------------------
# Application
# -----------------------------------------------------------------------------
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_APP_NAME=MyApp
# -----------------------------------------------------------------------------
# Database
# -----------------------------------------------------------------------------
# PostgreSQL connection string
# Format: postgres://USER:PASSWORD@HOST:PORT/DATABASE
DATABASE_URL=postgres://postgres:postgres@localhost:5432/myapp
# -----------------------------------------------------------------------------
# Authentication (Better Auth)
# -----------------------------------------------------------------------------
# Generate with: openssl rand -base64 32
BETTER_AUTH_SECRET=your-secret-key-min-32-characters-here
BETTER_AUTH_URL=http://localhost:3000
# OAuth Providers (get from provider console)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
# -----------------------------------------------------------------------------
# Third-Party Services (Optional)
# -----------------------------------------------------------------------------
# Analytics
# NEXT_PUBLIC_POSTHOG_KEY=
# NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
# Email (Resend)
# RESEND_API_KEY=
# Storage (S3/R2/Minio)
# S3_BUCKET=
# S3_REGION=
# S3_ACCESS_KEY_ID=
# S3_SECRET_ACCESS_KEY=
# S3_ENDPOINT=
# -----------------------------------------------------------------------------
# Development Only
# -----------------------------------------------------------------------------
# Set to 'true' to enable debug features
# DEBUG=false
Naming Conventions
Server-Only Variables (Default)
# Database
DATABASE_URL=...
# Auth secrets
BETTER_AUTH_SECRET=...
GOOGLE_CLIENT_SECRET=...
# API keys
RESEND_API_KEY=...
OPENAI_API_KEY=...
Client-Accessible Variables
Must start with NEXT_PUBLIC_:
# URLs and public identifiers
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_URLnotDB - 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
DATABASE_URL: z.string().url(),
// Auth
BETTER_AUTH_SECRET: z.string().min(32),
BETTER_AUTH_URL: z.string().url(),
// OAuth (optional in development)
GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(),
GITHUB_CLIENT_ID: z.string().optional(),
GITHUB_CLIENT_SECRET: z.string().optional(),
// Public
NEXT_PUBLIC_APP_URL: z.string().url(),
// Node environment
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});
// Validate at module load
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';
// Type-safe access
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
# ❌ Secrets in .env.example with real values
API_KEY=sk_live_real_key_here
# ❌ Secrets with NEXT_PUBLIC_ prefix
NEXT_PUBLIC_DATABASE_URL=postgres://...
# ❌ Committing .env.local
# (should be in .gitignore)
Always Do
# ✅ Placeholder values in .env.example
API_KEY=your-api-key-here
# ✅ Secrets stay server-only
DATABASE_URL=postgres://...
# ✅ Only public info gets NEXT_PUBLIC_
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
# Generate random secret (32 bytes, base64)
openssl rand -base64 32
# Generate random secret (hex)
openssl rand -hex 32
# Generate UUID
uuidgen
Per-Environment Configuration
Development
# .env.development
NODE_ENV=development
NEXT_PUBLIC_APP_URL=http://localhost:3000
Production
# Set in deployment platform (Railway, Vercel, AWS)
# Never in files committed to git
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
# .env.test
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
# Set via CLI
railway variables --set DATABASE_URL=postgres://...
railway variables --set BETTER_AUTH_SECRET=...
# Or use Railway dashboard
# Settings → Variables → Add Variable
Vercel
# Set via CLI
vercel env add DATABASE_URL production
vercel env add BETTER_AUTH_SECRET production
# Or use Vercel dashboard
# Settings → Environment Variables
AWS ECS
Use AWS Secrets Manager or Parameter Store:
# Store secret
aws secretsmanager create-secret \
--name myapp/production/database-url \
--secret-string "postgres://..."
# Reference in task definition
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:region:account:secret:myapp/production/database-url"
}
]
Debugging Environment Issues
Check What's Loaded
// Temporary debug (remove before commit!)
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
- [ ] Create
.env.examplewith all variables - [ ] Add
.env.localto.gitignore - [ ] Set up
src/lib/env.tsfor validation - [ ] Document required vs optional variables
- [ ] Generate secrets with proper randomness
- [ ] Configure deployment platform variables
- [ ] Test with production-like values locally
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 |
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.