Environment Management
Quick Guide: Per-app .env files (apps/client-next/.env). Framework-specific prefixes (NEXTPUBLIC_, VITE__). Zod validation at startup. Maintain .env.example templates. Never commit secrets (.gitignore). Environment-based feature flags.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type , named constants)
(You MUST validate ALL environment variables with Zod at application startup)
(You MUST use framework-specific prefixes for client-side variables - NEXTPUBLIC* for Next.js, VITE_* for Vite)
(You MUST maintain .env.example templates with ALL required variables documented)
(You MUST never commit secrets to version control - use .env.local and CI secrets)
(You MUST use per-app .env files - NOT root-level .env files)
</critical_requirements>
Auto-detection: Environment variables, .env files, Zod validation, t3-env, @t3-oss/env, secrets management, NEXTPUBLIC prefix, VITE_ prefix, feature flags, z.stringbool
When to use:
-
Setting up Zod validation for type-safe environment variables at startup
-
Managing per-app .env files with framework-specific prefixes
-
Securing secrets (never commit, use .env.local and CI secrets)
-
Implementing environment-based feature flags
When NOT to use:
-
Runtime configuration changes (use external feature flag services like LaunchDarkly)
-
User-specific settings (use database or user preferences)
-
Frequently changing values (use configuration API or database)
-
Complex A/B testing with gradual rollouts (use dedicated feature flag services)
Key patterns covered:
-
Per-app .env files (not root-level, prevents conflicts)
-
Zod validation at startup for type safety and early failure
-
T3 Env pattern for Next.js/Vite projects (recommended)
-
Framework-specific prefixes (NEXTPUBLIC_ for client, VITE__ for Vite client)
-
.env.example templates for documentation and onboarding
Detailed Resources:
-
For code examples, see examples/ folder:
-
examples/core.md - Essential patterns (per-app .env, Zod validation)
-
examples/t3-env.md - T3 Env pattern for Next.js/Vite (recommended)
-
examples/naming-and-templates.md - Framework prefixes, .env.example
-
examples/security-and-secrets.md - Secret management
-
examples/feature-flags-and-config.md - Feature flags, centralized config
-
For decision frameworks and anti-patterns, see reference.md
Philosophy
Environment management follows the principle that configuration is code - it should be validated, typed, and versioned. The system uses per-app .env files with framework-specific prefixes, Zod validation at startup, and strict security practices to prevent secret exposure.
When to use this environment management approach:
-
Managing environment-specific configuration (API URLs, feature flags, credentials)
-
Setting up type-safe environment variables with Zod validation
-
Securing secrets with .gitignore and CI/CD secret management
-
Implementing feature flags without external services
-
Documenting required environment variables for team onboarding
When NOT to use:
-
Runtime configuration changes (use external feature flag services like LaunchDarkly)
-
User-specific settings (use database or user preferences)
-
Frequently changing values (use configuration API or database)
Core Patterns
Pattern 1: Per-App Environment Files
Each app/package has its own .env file to prevent conflicts and clarify ownership.
File Structure
apps/ ├── client-next/ │ ├── .env # Local development (NEXT_PUBLIC_API_URL) │ └── .env.production # Production overrides ├── client-react/ │ ├── .env # Local development │ └── .env.production # Production overrides └── server/ ├── .env # Local server config ├── .env.example # Template for new developers └── .env.local.example # Local overrides template
packages/ ├── api/ │ └── .env # API package config └── api-mocks/ └── .env # Mock server config
File Types and Purpose
-
.env
-
Default development values (committed for apps, gitignored for sensitive packages)
-
.env.example
-
Documentation template (committed, shows all required variables)
-
.env.local
-
Local developer overrides (gitignored, takes precedence over .env )
-
.env.production
-
Production configuration (committed or in CI secrets)
-
.env.local.example
-
Local override template (committed)
Loading Order and Precedence
Next.js loading order (highest to lowest priority):
-
.env.$(NODE_ENV).local (e.g., .env.production.local )
-
.env.local (not loaded when NODE_ENV=test )
-
.env.$(NODE_ENV) (e.g., .env.production )
-
.env
Vite loading order:
-
.env.[mode].local (e.g., .env.production.local )
-
.env.[mode] (e.g., .env.production )
-
.env.local
-
.env
Exception: Shared variables can go in turbo.json env array (see setup/monorepo/basic.md)
See examples/core.md for complete code examples.
Pattern 2: Type-Safe Environment Variables with Zod
Validate environment variables at application startup using Zod schemas.
Constants
const DEFAULT_API_TIMEOUT_MS = 30000; const DEFAULT_API_RETRY_ATTEMPTS = 3;
Validation Schema
// lib/env.ts import { z } from "zod";
const DEFAULT_API_TIMEOUT_MS = 30000;
const envSchema = z.object({ // Public variables (VITE_ prefix) VITE_API_URL: z.string().url(), VITE_API_TIMEOUT: z.coerce.number().default(DEFAULT_API_TIMEOUT_MS), // Use z.stringbool() for boolean env vars (Zod 4+) // Handles "true"/"false"/"1"/"0"/"yes"/"no" correctly VITE_ENABLE_ANALYTICS: z.stringbool().default(false), VITE_ENVIRONMENT: z.enum(["development", "staging", "production"]),
// Build-time variables MODE: z.enum(["development", "production"]), DEV: z.boolean(), PROD: z.boolean(), });
// Validate and export
function validateEnv() {
try {
return envSchema.parse(import.meta.env);
} catch (error) {
if (error instanceof z.ZodError) {
console.error("Invalid environment variables:");
error.errors.forEach((err) => {
console.error( - ${err.path.join(".")}: ${err.message});
});
throw new Error("Invalid environment configuration");
}
throw error;
}
}
export const env = validateEnv();
// Type-safe usage console.log(env.VITE_API_URL); // string console.log(env.VITE_API_TIMEOUT); // number console.log(env.VITE_ENABLE_ANALYTICS); // boolean
Why good: Type safety prevents runtime errors from typos or wrong types, runtime validation fails fast at startup with clear error messages, default values reduce required configuration, IDE autocomplete improves DX
Note: For Next.js/Vite projects, consider using T3 Env (@t3-oss/env-nextjs or @t3-oss/env-core ) which provides additional features like client/server variable separation and build-time validation. See examples/t3-env.md.
See examples/core.md for complete good/bad comparisons.
Pattern 3: Framework-Specific Naming Conventions
Use framework-specific prefixes for client-side variables and SCREAMING_SNAKE_CASE for all environment variables.
Mandatory Conventions
-
SCREAMING_SNAKE_CASE - All environment variables use uppercase with underscores
-
Descriptive names - Variable names clearly indicate purpose
-
Framework prefixes - Use NEXT_PUBLIC_* (Next.js) or VITE_* (Vite) for client-side variables
Framework Prefixes
Next.js:
-
NEXT_PUBLIC_*
-
Client-side accessible (embedded in bundle) - use for API URLs, public keys, feature flags
-
No prefix - Server-side only (database URLs, secret keys, API tokens)
Vite:
-
VITE_*
-
Client-side accessible (embedded in bundle) - use for API URLs, public configuration
-
No prefix - Build-time only (not exposed to client)
Node.js/Server:
-
NODE_ENV
-
Standard environment (development , production , test )
-
PORT
-
Server port number
-
No prefix - All variables available server-side
See examples/naming-and-templates.md for complete code examples with good/bad comparisons.
Integration Guide
Works with:
-
Zod: Runtime validation and type inference for environment variables
-
T3 Env: Recommended wrapper for Zod validation with client/server separation (@t3-oss/env-nextjs , @t3-oss/env-core )
-
Turborepo: Declare shared env vars in turbo.json for cache invalidation (see setup/monorepo/basic.md)
-
CI/CD: GitHub Secrets, Vercel Environment Variables for production secrets (see backend/ci-cd/basic.md)
-
Next.js: Automatic .env file loading with NEXTPUBLIC* prefix for client-side
-
Vite: Automatic .env file loading with VITE_* prefix for client-side
Replaces / Conflicts with:
-
Hardcoded configuration values (use env vars instead)
-
Runtime feature flag services for simple boolean flags (use env vars first, upgrade to LaunchDarkly if needed)
<decision_framework>
Decision Framework
Need environment configuration? ├─ Is it a secret (API key, password)? │ ├─ YES → Use .env.local (gitignored) + CI secrets │ └─ NO → Can it be public (embedded in client bundle)? │ ├─ YES → Use NEXT_PUBLIC_* or VITE_* prefix │ └─ NO → Server-side only (no prefix) ├─ Does it change per environment? │ ├─ YES → Use .env.{environment} files │ └─ NO → Use .env with defaults └─ Is it app-specific or shared? ├─ App-specific → Per-app .env file └─ Shared → Declare in turbo.json env array
See reference.md for complete decision frameworks including feature flag decisions.
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
-
Committing secrets to version control (.env files with real credentials)
-
Using environment variables directly without Zod validation (causes runtime errors)
-
Using NEXTPUBLIC_ or VITE__ prefix for secrets (embeds in client bundle)
Medium Priority Issues:
-
Missing .env.example documentation (poor onboarding experience)
-
Using production secrets in development (security risk)
-
Root-level .env in monorepo (causes conflicts)
Gotchas:
-
Next.js/Vite embed prefixed variables at build time, not runtime - requires rebuild to change
-
Environment variables are strings - use z.coerce.number() for numbers, use z.stringbool() for booleans (Zod 4+)
-
CRITICAL: z.coerce.boolean() converts "false" to true (string is truthy) - use z.stringbool() instead
-
Empty string env vars are NOT undefined
-
use T3 Env's emptyStringAsUndefined: true option
-
Turborepo cache is NOT invalidated by env changes unless declared in turbo.json env array
See reference.md for complete RED FLAGS, anti-patterns, and checklists.
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST validate ALL environment variables with Zod at application startup)
(You MUST use framework-specific prefixes for client-side variables - NEXTPUBLIC* for Next.js, VITE_* for Vite)
(You MUST maintain .env.example templates with ALL required variables documented)
(You MUST never commit secrets to version control - use .env.local and CI secrets)
(You MUST use per-app .env files - NOT root-level .env files)
Failure to follow these rules will cause runtime errors, security vulnerabilities, and configuration confusion.
</critical_reminders>