Skip to content
infra-env logo

Environment Variables Skill

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.

SKILL.md

Full skill instructions

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

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View