Claude-skill-registry infra-env
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.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/infra-env" ~/.claude/skills/majiayu000-claude-skill-registry-infra-env && rm -rf "$T"
manifest:
skills/data/infra-env/SKILL.mdsource content
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
templates.env.example - Troubleshooting environment issues
Core Principles
- Never commit secrets -
files with real values stay out of git.env* - Document with examples -
shows structure without values.env.example - Prefix client-safe vars - Only
reaches the browserNEXT_PUBLIC_* - Validate at startup - Fail fast if required vars are missing
- Use typed configuration - Type-safe access to environment
File Hierarchy
| File | Purpose | Git? |
|---|---|---|
| Default values (shared) | Optional |
| Local overrides (secrets) | Never |
| Dev-specific defaults | Optional |
| Prod defaults (no secrets) | Optional |
| Template for developers | Always |
Load order (later overrides earlier):
.env.env.local
/.env.development
(based on NODE_ENV).env.production
/.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:
notDATABASE_URLDB - Group by service:
,GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRET - Prefix third-party:
,RESEND_API_KEYSTRIPE_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 (
not.env.local
).env.local.txt - Restart dev server after adding new variables
- Verify variable name matches exactly (case-sensitive)
Client can't access variable:
- Must have
prefixNEXT_PUBLIC_ - Must rebuild after adding (for production builds)
Wrong value used:
- Check load order (
overrides.env.local
).env - Clear
cache:.nextrm -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
with all variables.env.example - Add
to.env.local.gitignore - Set up
for validationsrc/lib/env.ts - 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 | | Server + Client |
| File | Committed? | Contains Secrets? |
|---|---|---|
| Yes | No (placeholders) |
| Optional | No |
| No | Yes |