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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "infra-env" with this command: npx skills add aussiegingersnap/cursor-skills/aussiegingersnap-cursor-skills-infra-env

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

  1. Never commit secrets - .env* files with real values stay out of git
  2. Document with examples - .env.example shows structure without values
  3. Prefix client-safe vars - Only NEXT_PUBLIC_* reaches the browser
  4. Validate at startup - Fail fast if required vars are missing
  5. Use typed configuration - Type-safe access to environment

File Hierarchy

FilePurposeGit?
.envDefault values (shared)Optional
.env.localLocal overrides (secrets)Never
.env.developmentDev-specific defaultsOptional
.env.productionProd defaults (no secrets)Optional
.env.exampleTemplate for developersAlways

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_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
  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

  1. Copy template: cp .env.example .env.local
  2. Fill in values for local services
  3. Start Docker services: docker compose up -d
  4. 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.example with all variables
  • Add .env.local to .gitignore
  • Set up src/lib/env.ts for validation
  • Document required vs optional variables
  • Generate secrets with proper randomness
  • Configure deployment platform variables
  • Test with production-like values locally

Quick Reference

Variable TypePrefixAccessible In
Server secretNoneServer only
Public configNEXT_PUBLIC_Server + Client
FileCommitted?Contains Secrets?
.env.exampleYesNo (placeholders)
.envOptionalNo
.env.localNoYes

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tools-repo-review

No summary provided by upstream source.

Repository SourceNeeds Review
General

db-postgres

No summary provided by upstream source.

Repository SourceNeeds Review
General

ui-principles

No summary provided by upstream source.

Repository SourceNeeds Review