infra-config-setup-env
Environment configuration, Zod validation
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/infra-config-setup-env/skills/infra-config-setup-env
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Environment Management
Quick Guide: Per-app .env files. Framework-specific prefixes (
NEXT_PUBLIC_*for Next.js,VITE_*for 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 - NEXT_PUBLIC_* 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, NEXT_PUBLIC_ 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 an external feature flag service)
- User-specific settings (use database or user preferences)
- Frequently changing values (use configuration API or database)
- Complex A/B testing with gradual rollouts (use a dedicated feature flag service)
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 (
NEXT_PUBLIC_*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
<decision_framework>
Decision Framework
See reference.md for complete decision frameworks including environment configuration and 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
NEXT_PUBLIC_*orVITE_*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, usez.stringbool()for booleans (Zod 4+) - CRITICAL:
z.coerce.boolean()converts "false" totrue(string is truthy) - usez.stringbool()(Zod 4+) instead - Empty string env vars are NOT
undefined- use T3 Env'semptyStringAsUndefined: trueoption - Monorepo build tool caches may NOT be invalidated by env changes unless declared in the tool's env configuration
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 - NEXT_PUBLIC_* 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>
Files (skills)
-
examples
-
core.md 3.7 KB
# Environment Core Examples > Essential environment configuration patterns. See [SKILL.md](../SKILL.md) for core concepts and [reference.md](../reference.md) for decision frameworks. **Additional Examples:** - [naming-and-templates.md](naming-and-templates.md) - Framework prefixes, .env.example - [security-and-secrets.md](security-and-secrets.md) - Secret management - [feature-flags-and-config.md](feature-flags-and-config.md) - Feature flags, centralized config --- ## Per-App Environment Files ### Good Example - Per-app environment files ```typescript // apps/client-next/.env NEXT_PUBLIC_API_URL=http://localhost:3000/api/v1 // apps/server/.env.example # Base configuration NODE_ENV=development PORT=1337 ``` **Why good:** Per-app configuration prevents conflicts in monorepo, clear defaults reduce onboarding friction, .env.example serves as documentation template ### Bad Example - Root-level .env ```typescript // .env (root level - AVOID) NEXT_PUBLIC_API_URL=http://localhost:3000/api/v1 DATABASE_URL=postgresql://localhost:5432/mydb PORT=1337 ``` **Why bad:** Root-level .env causes shared variables across apps with different needs, larger blast radius when misconfigured, unclear ownership --- ## Type-Safe Environment Variables with Zod ### Good Example - Zod validation at startup ```typescript // 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+) // Correctly handles "true"/"false"/"1"/"0"/"yes"/"no" 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.issues.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 ### Bad Example - No validation ```typescript // lib/config.ts const API_URL = import.meta.env.VITE_API_URL; // Could be undefined! const TIMEOUT = Number(import.meta.env.VITE_API_TIMEOUT); // Could be NaN! ``` **Why bad:** No validation means runtime failures with unclear error messages, type coercion fails silently (NaN), missing variables only discovered during usage not startup --- ### Bad Example - Using z.coerce.boolean() for env vars ```typescript // lib/env.ts - WRONG! const envSchema = z.object({ // DON'T use z.coerce.boolean() for env vars! VITE_ENABLE_ANALYTICS: z.coerce.boolean().default(false), }); // Problem: z.coerce.boolean() uses JavaScript's Boolean() // Boolean("false") === true (non-empty string is truthy!) // Boolean("0") === true // This breaks env var semantics where "false" should mean false ``` **Why bad:** `z.coerce.boolean()` converts ANY non-empty string to `true` including "false", "0", "no" - use `z.stringbool()` (Zod 4+) which correctly parses "true"/"false"/"1"/"0"/"yes"/"no"/"on"/"off"/"enabled"/"disabled" -
feature-flags-and-config.md 4.7 KB
# Feature Flags and Configuration > Environment-based feature flags and centralized configuration patterns. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for essential patterns. **Related Examples:** - [core.md](core.md) - Per-app .env, Zod validation - [naming-and-templates.md](naming-and-templates.md) - Framework prefixes, .env.example - [security-and-secrets.md](security-and-secrets.md) - Secret management --- ## Feature Flags with Environment Variables ### Good Example - Type-safe feature flags ```typescript // lib/feature-flags.ts export const FEATURES = { // Core features NEW_DASHBOARD: import.meta.env.VITE_FEATURE_NEW_DASHBOARD === "true", BETA_EDITOR: import.meta.env.VITE_FEATURE_BETA_EDITOR === "true", // Analytics & Monitoring ANALYTICS: import.meta.env.VITE_ENABLE_ANALYTICS === "true", ERROR_TRACKING: import.meta.env.VITE_ENABLE_ERROR_TRACKING === "true", // Environment-specific DEBUG_MODE: import.meta.env.DEV, MOCK_API: import.meta.env.VITE_MOCK_API === "true", } as const; // Type-safe feature check export function isFeatureEnabled(feature: keyof typeof FEATURES): boolean { return FEATURES[feature]; } export const { NEW_DASHBOARD, BETA_EDITOR, ANALYTICS } = FEATURES; ``` ```typescript // Usage in components import { NEW_DASHBOARD } from "./feature-flags"; import { lazy } from "react"; // Code splitting based on feature flag const Dashboard = NEW_DASHBOARD ? lazy(() => import("./features/dashboard-v2")) : lazy(() => import("./features/dashboard-v1")); ``` **Why good:** Type-safe flags prevent typos, centralized configuration makes flags discoverable, code splitting reduces bundle size for disabled features, no external dependencies reduces complexity ### Bad Example - Inline feature checks ```typescript // components/dashboard.tsx function Dashboard() { // Reading env var directly everywhere if (process.env.NEXT_PUBLIC_FEATURE_NEW_DASHBOARD === "true") { return <NewDashboard />; } return <LegacyDashboard />; } ``` **Why bad:** Inline checks scatter feature flag logic across codebase, no type safety means typos fail silently, hard to discover all feature flags, duplicated string comparisons --- ## Environment-Specific Configuration ### Good Example - Centralized config with env overrides ```typescript // lib/config.ts import { env } from "./env"; const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000; const DEFAULT_CACHE_MAX_SIZE = 100; const DEFAULT_RETRY_ATTEMPTS = 3; interface AppConfig { api: { baseUrl: string; timeout: number; retryAttempts: number; }; features: { analytics: boolean; errorTracking: boolean; debugMode: boolean; }; cache: { ttl: number; maxSize: number; }; } function getConfig(): AppConfig { const baseConfig: AppConfig = { api: { baseUrl: env.VITE_API_URL, timeout: env.VITE_API_TIMEOUT, retryAttempts: DEFAULT_RETRY_ATTEMPTS, }, features: { analytics: env.VITE_ENABLE_ANALYTICS, errorTracking: false, debugMode: env.DEV, }, cache: { ttl: DEFAULT_CACHE_TTL_MS, maxSize: DEFAULT_CACHE_MAX_SIZE, }, }; // Environment-specific overrides const envConfigs: Record<string, Partial<AppConfig>> = { development: { cache: { ttl: 0, maxSize: 0 }, // No caching in dev features: { debugMode: true }, }, production: { cache: { ttl: 15 * 60 * 1000, maxSize: 500 }, features: { errorTracking: true, debugMode: false }, }, }; const envOverrides = envConfigs[env.VITE_ENVIRONMENT] || {}; return { ...baseConfig, ...envOverrides, api: { ...baseConfig.api, ...envOverrides.api }, features: { ...baseConfig.features, ...envOverrides.features }, cache: { ...baseConfig.cache, ...envOverrides.cache }, }; } export const config = getConfig(); ``` ```typescript // Usage import { config } from "./config"; fetch(config.api.baseUrl, { signal: AbortSignal.timeout(config.api.timeout), }); if (config.features.analytics) { trackEvent("page_view"); } ``` **Why good:** Centralized configuration provides single source of truth for app behavior, environment-specific overrides enable different settings per environment (dev vs prod), type-safe access prevents runtime errors from typos, easy to test with mock config injection ### Bad Example - Scattered configuration ```typescript // Multiple files reading env vars directly // api-client.ts const timeout = Number(process.env.VITE_API_TIMEOUT); // analytics.ts const enabled = process.env.VITE_ENABLE_ANALYTICS === "true"; // cache.ts const ttl = process.env.VITE_CACHE_TTL || "300000"; ``` **Why bad:** Scattered configuration makes it hard to understand app behavior, no centralized type safety, duplicated env var parsing logic, difficult to test or mock -
naming-and-templates.md 3.7 KB
# Naming Conventions and Templates > Framework-specific naming and .env.example documentation patterns. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for essential patterns. **Related Examples:** - [core.md](core.md) - Per-app .env, Zod validation - [security-and-secrets.md](security-and-secrets.md) - Secret management - [feature-flags-and-config.md](feature-flags-and-config.md) - Feature flags, centralized config --- ## Framework-Specific Naming Conventions ### Good Example - Framework-specific prefixes ```bash # apps/client-next/.env # Client-side variables (embedded in bundle) NEXT_PUBLIC_API_URL=http://localhost:3000/api/v1 NEXT_PUBLIC_ANALYTICS_ID=UA-123456789-1 NEXT_PUBLIC_ENVIRONMENT=development NEXT_PUBLIC_FEATURE_NEW_DASHBOARD=true # Server-side variables (not exposed to client) DATABASE_URL=postgresql://localhost:5432/mydb API_SECRET_KEY=super-secret-key-12345 STRIPE_SECRET_KEY=sk_test_... JWT_SECRET=jwt-secret-key ``` **Why good:** `NEXT_PUBLIC_*` prefix makes client-side variables explicit preventing accidental secret exposure, server-side variables never embedded in bundle, clear separation improves security ### Bad Example - Missing prefixes and poor naming ```bash # .env # No framework prefix - unclear if client-side API_URL=http://localhost:3000/api/v1 # Inconsistent casing apiUrl=https://api.example.com Database_Url=postgresql://localhost/db # Unclear names URL=https://api.example.com KEY=12345 FLAG=true ``` **Why bad:** Missing framework prefix makes it unclear if variable is client-side or server-side, inconsistent casing reduces readability, unclear names make purpose ambiguous --- ## .env.example Templates ### Good Example - Comprehensive .env.example ```bash # .env.example # ================================================================ # IMPORTANT: Copy this file to .env and fill in the values # ================================================================ # cp .env.example .env # ==================================== # API Configuration (Required) # ==================================== # Base URL for API requests # Development: http://localhost:3000/api/v1 # Production: https://api.example.com/api/v1 NEXT_PUBLIC_API_URL=http://localhost:3000/api/v1 # API request timeout in milliseconds (optional, default: 30000) # Range: 1000-60000 NEXT_PUBLIC_API_TIMEOUT_MS=30000 # Number of retry attempts (optional, default: 3) NEXT_PUBLIC_API_RETRY_ATTEMPTS=3 # ==================================== # Database Configuration (Server-side) # ==================================== # PostgreSQL connection string (required for server) # Format: postgresql://username:password@host:port/database DATABASE_URL= # Database pool size (optional, default: 10) DATABASE_POOL_SIZE=10 # ==================================== # Feature Flags (Optional) # ==================================== # Enable new dashboard (default: false) NEXT_PUBLIC_FEATURE_NEW_DASHBOARD=false # ==================================== # Third-Party Services (Optional) # ==================================== # Stripe public key # Get from: https://dashboard.stripe.com/apikeys NEXT_PUBLIC_STRIPE_PUBLIC_KEY= # Stripe secret key (server-side only) # WARNING: NEVER commit this to version control STRIPE_SECRET_KEY= ``` **Why good:** Grouped related variables for easy navigation, comments explain purpose and format reducing onboarding friction, example values show expected format, links to third-party services speed up setup ### Bad Example - Poor .env.example ```bash # .env.example NEXT_PUBLIC_API_URL= DATABASE_URL= STRIPE_SECRET_KEY= ``` **Why bad:** No comments explaining purpose or format, no grouping makes it hard to find related variables, no example values leaving developers guessing, no links to get third-party keys -
security-and-secrets.md 2.7 KB
# Security and Secrets Management > Patterns for secure secret handling. See [SKILL.md](../SKILL.md) for core concepts and [core.md](core.md) for essential patterns. **Related Examples:** - [core.md](core.md) - Per-app .env, Zod validation - [naming-and-templates.md](naming-and-templates.md) - Framework prefixes, .env.example - [feature-flags-and-config.md](feature-flags-and-config.md) - Feature flags, centralized config --- ## Secret Management ### Good Example - Comprehensive .gitignore ```gitignore # .gitignore # Environment files .env.local .env.*.local # Optional: ignore all .env files except example # .env # !.env.example # Sensitive files *.key *.pem *.p12 *.pfx ``` **Why good:** `.env.local` and `.env.*.local` patterns prevent committing local secrets, sensitive file extensions (`*.key`, `*.pem`) prevent accidental key commits, optional .env ignore with `!.env.example` allows flexibility ### Bad Example - Secrets committed to repository ```bash # .env (committed with actual secrets) DATABASE_URL=postgresql://admin:SuperSecret123@prod.example.com:5432/mydb STRIPE_SECRET_KEY=sk_live_actual_secret_key JWT_SECRET=my-production-jwt-secret # Committed to git = security breach! ``` **Why bad:** Committing secrets to git exposes them permanently in history, anyone with repo access can extract production credentials, secret rotation requires coordinating with all developers --- ## Secret Distribution Patterns ### Good Example - CI/CD Secret Management ```yaml # .github/workflows/deploy.yml jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Deploy env: DATABASE_URL: ${{ secrets.DATABASE_URL }} STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }} run: | # Secrets available as environment variables npm run deploy ``` **Why good:** Secrets stored in CI/CD provider (GitHub Secrets, Vercel), never committed to repository, access controlled by repository permissions, easy to rotate without code changes ### Bad Example - Sharing secrets via chat ``` # Slack message (NEVER DO THIS) Hey team, here's the database password: SuperSecret123 Just copy it into your .env file ``` **Why bad:** Secrets in chat logs are permanent and searchable, no access control or audit trail, impossible to rotate without coordinating with everyone, violates security compliance requirements --- ## Secret Rotation Checklist When rotating secrets: 1. Generate new secret in the service dashboard 2. Update CI/CD secrets (GitHub, Vercel, etc.) 3. Deploy with new secret 4. Verify application works with new secret 5. Revoke old secret in service dashboard 6. Never store old secrets "just in case" -
t3-env.md 6.5 KB
# T3 Env Pattern > T3 Env (`@t3-oss/env-nextjs`, `@t3-oss/env-core`) provides type-safe environment variables with client/server separation. See [SKILL.md](../SKILL.md) for core concepts. **Related Examples:** - [core.md](core.md) - Basic Zod validation pattern - [naming-and-templates.md](naming-and-templates.md) - Framework prefixes, .env.example - [security-and-secrets.md](security-and-secrets.md) - Secret management --- ## T3 Env for Next.js ### Good Example - T3 Env with Next.js ```typescript // app/env.ts import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; export const env = createEnv({ // Server-side variables (never exposed to client) server: { DATABASE_URL: z.string().url(), API_SECRET_KEY: z.string().min(1), STRIPE_SECRET_KEY: z.string().min(1), }, // Client-side variables (embedded in bundle) client: { NEXT_PUBLIC_API_URL: z.string().url(), NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().min(1), // Use z.stringbool() for boolean env vars (Zod 4+) NEXT_PUBLIC_ENABLE_ANALYTICS: z.stringbool().default(false), }, // Explicit runtime env mapping (required for tree-shaking) runtimeEnv: { DATABASE_URL: process.env.DATABASE_URL, API_SECRET_KEY: process.env.API_SECRET_KEY, STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, NEXT_PUBLIC_ENABLE_ANALYTICS: process.env.NEXT_PUBLIC_ENABLE_ANALYTICS, }, // Treat empty strings as undefined (recommended) emptyStringAsUndefined: true, }); ``` **Why good:** Separates client/server variables preventing accidental secret exposure, tree-shaking ensures only accessed vars included in bundle, emptyStringAsUndefined handles blank .env values correctly --- ## Build-Time Validation ### Good Example - Validate at build time via next.config ```typescript // next.config.ts (Next.js 15+ - native TypeScript support, no jiti needed) import "./app/env"; // Validates env vars at build time import type { NextConfig } from "next"; const nextConfig: NextConfig = { // Your config here }; export default nextConfig; // next.config requires default export ``` ```typescript // next.config.mjs (Next.js < 15, uses jiti for TypeScript) import { fileURLToPath } from "node:url"; import createJiti from "jiti"; const jiti = createJiti(fileURLToPath(import.meta.url)); // Validate env vars at build time jiti("./app/env"); /** @type {import('next').NextConfig} */ const nextConfig = { // Your config here }; export default nextConfig; // next.config requires default export ``` **Why good:** Build fails immediately if env vars are missing or invalid, prevents deploying broken builds, catches configuration errors before runtime --- ## T3 Env for Vite ### Good Example - T3 Env Core with Vite ```typescript // src/env.ts import { createEnv } from "@t3-oss/env-core"; import { z } from "zod"; export const env = createEnv({ // Client-side variables (Vite uses VITE_ prefix) clientPrefix: "VITE_", client: { VITE_API_URL: z.string().url(), VITE_ENABLE_ANALYTICS: z.stringbool().default(false), VITE_ENVIRONMENT: z.enum(["development", "staging", "production"]), }, // Server variables (build-time only in Vite) server: { // Vite doesn't expose non-prefixed vars to client API_SECRET: z.string().min(1).optional(), }, runtimeEnv: import.meta.env, emptyStringAsUndefined: true, }); ``` **Why good:** Uses Vite's VITE\_ prefix convention, works with import.meta.env, type-safe access throughout app --- ## Skip Validation for CI/Linting ### Good Example - Skip validation when env vars not needed ```typescript // app/env.ts import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; export const env = createEnv({ server: { DATABASE_URL: z.string().url(), }, client: { NEXT_PUBLIC_API_URL: z.string().url(), }, runtimeEnv: { DATABASE_URL: process.env.DATABASE_URL, NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, }, // Skip validation for linting/type-checking in CI skipValidation: !!process.env.SKIP_ENV_VALIDATION, }); ``` **Why good:** Allows running type checks and linting in CI without setting all env vars, controlled via SKIP_ENV_VALIDATION flag, still validates in actual builds/runtime --- ## Custom Error Handling ### Good Example - Custom validation error handler ```typescript // app/env.ts import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; export const env = createEnv({ server: { DATABASE_URL: z.string().url(), }, client: { NEXT_PUBLIC_API_URL: z.string().url(), }, runtimeEnv: { DATABASE_URL: process.env.DATABASE_URL, NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, }, // Custom error handler for validation failures onValidationError: (error) => { console.error("Invalid environment variables:"); console.error(error.flatten().fieldErrors); throw new Error("Invalid environment configuration"); }, // Custom handler for accessing server vars on client onInvalidAccess: (variable) => { throw new Error( `Attempted to access server-side env var "${variable}" on the client`, ); }, }); ``` **Why good:** Custom error messages improve debugging, onInvalidAccess prevents silent failures when server vars accessed on client --- ## Type-Safe Usage ```typescript // Any file in your app import { env } from "~/env"; // Same import everywhere // Server-side (API routes, server components, etc.) const dbUrl = env.DATABASE_URL; // string - type-safe! // Client-side (client components, hooks, etc.) const apiUrl = env.NEXT_PUBLIC_API_URL; // string - type-safe! // Accessing server var on client throws error at runtime const secret = env.DATABASE_URL; // Error: Can't access server var on client ``` --- ## Anti-Patterns ### Bad Example - Manual process.env access ```typescript // components/api-client.ts - WRONG! const apiUrl = process.env.NEXT_PUBLIC_API_URL; // No type safety! const timeout = Number(process.env.NEXT_PUBLIC_TIMEOUT); // Could be NaN! ``` **Why bad:** No validation, no type safety, silent failures at runtime ### Bad Example - Missing runtimeEnv mapping ```typescript // app/env.ts - WRONG! export const env = createEnv({ server: { DATABASE_URL: z.string().url(), }, // Missing runtimeEnv - Next.js won't include the var in bundle! }); ``` **Why bad:** Next.js tree-shakes unused process.env access, missing runtimeEnv means vars are undefined at runtime
-
-
reference.md 7.5 KB
# Environment Reference > Decision frameworks, anti-patterns, and red flags for environment configuration. See [SKILL.md](SKILL.md) for core concepts and [examples/](examples/) for code examples. --- ## 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 ├─ Does it need validation? │ ├─ YES → Add to Zod schema (recommended for all) │ └─ NO → Document in .env.example at minimum └─ Is it app-specific or shared? ├─ App-specific → Per-app .env file └─ Shared → Declare in build tool's env configuration ``` ### Feature Flag Decision ``` Need a feature flag? ├─ Is it a simple boolean toggle? │ ├─ YES → Use environment variable │ └─ NO → Need gradual rollout (5% → 50% → 100%)? │ ├─ YES → Use an external feature flag service │ └─ NO → Need user targeting? │ ├─ YES → Use an external feature flag service │ └─ NO → Use environment variable ``` --- ## 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 `NEXT_PUBLIC_*` or `VITE_*` prefix for secrets (embeds in client bundle) - Sharing .env files via Slack/email (insecure secret distribution) ### Medium Priority Issues - Missing .env.example documentation (poor onboarding experience) - Using production secrets in development (security risk) - Not rotating secrets regularly (stale credentials) - Inconsistent variable names across environments (confusion) ### Common Mistakes - Using `process.env.VARIABLE` directly without validation (fails at runtime with unclear errors) - Forgetting to add new variables to .env.example (team members don't know about them) - Not using framework-specific prefixes for client-side variables (values are undefined) - Using root-level .env instead of per-app .env files (conflicts in monorepo) ### Gotchas & Edge Cases - Next.js embeds `NEXT_PUBLIC_*` variables at build time (not runtime) - requires rebuild to change - Vite embeds `VITE_*` variables at build time - same limitation as Next.js - Environment variables are strings - use `z.coerce.number()` for numbers - **CRITICAL: `z.coerce.boolean()` converts "false" to `true`** - JavaScript's `Boolean("false")` is `true` (non-empty string is truthy). Use `z.stringbool()` (Zod 4+) instead which correctly handles "true"/"false"/"1"/"0"/"yes"/"no"/"on"/"off"/"enabled"/"disabled" - Empty string env vars (`PORT=`) are NOT `undefined` - use T3 Env's `emptyStringAsUndefined: true` or handle explicitly - .env.local takes precedence over .env - can cause confusion when local overrides exist - Monorepo build tool caches may NOT be invalidated by env changes unless declared in the tool's env configuration - Next.js tree-shakes unused `process.env` access - use T3 Env's `runtimeEnv` for explicit mapping --- ## Anti-Patterns ### Committing Secrets to Repository ```bash # ANTI-PATTERN: Real secrets in committed .env DATABASE_URL=postgresql://admin:SuperSecret123@prod.example.com:5432/mydb STRIPE_SECRET_KEY=sk_live_actual_secret_key ``` **Why it's wrong:** Exposes secrets permanently in git history, anyone with repo access can extract credentials. **What to do instead:** Use .env.local (gitignored) for secrets, CI/CD secrets for production. --- ### No Zod Validation ```typescript // ANTI-PATTERN: Direct env access without validation const API_URL = import.meta.env.VITE_API_URL; // Could be undefined! const TIMEOUT = Number(import.meta.env.VITE_API_TIMEOUT); // Could be NaN! ``` **Why it's wrong:** Missing variables cause runtime failures with unclear errors, type coercion fails silently. **What to do instead:** Validate all env vars with Zod at application startup. --- ### Using z.coerce.boolean() for Env Vars ```typescript // ANTI-PATTERN: z.coerce.boolean() breaks on "false" string const envSchema = z.object({ VITE_ENABLE_FEATURE: z.coerce.boolean().default(false), }); // .env file: VITE_ENABLE_FEATURE=false // Result: true (!) // z.coerce.boolean() uses JavaScript's Boolean() // Boolean("false") === true because non-empty strings are truthy ``` **Why it's wrong:** `z.coerce.boolean()` converts ANY non-empty string to `true`, including "false", "0", "no". This breaks environment variable semantics. **What to do instead:** Use `z.stringbool()` (Zod 4+) which correctly parses "true"/"false"/"1"/"0"/"yes"/"no"/"on"/"off"/"enabled"/"disabled". --- ### Using `NEXT_PUBLIC_*` for Secrets ```bash # ANTI-PATTERN: Secret with client-side prefix NEXT_PUBLIC_DATABASE_URL=postgresql://user:pass@host/db NEXT_PUBLIC_API_SECRET_KEY=sk_secret_12345 ``` **Why it's wrong:** `NEXT_PUBLIC_*` variables are embedded in client bundle, visible to anyone. **What to do instead:** Use non-prefixed variables for server-side secrets only. --- ### Root-Level .env in Monorepo ``` # ANTI-PATTERN: Root-level .env /.env <- Variables for all apps (conflicts!) ``` **Why it's wrong:** Shared variables cause conflicts across apps with different needs, unclear ownership. **What to do instead:** Use per-app .env files in each app directory. --- ### Inline Feature Flag Checks ```typescript // ANTI-PATTERN: Inline env checks scattered across codebase if (process.env.NEXT_PUBLIC_FEATURE_NEW_DASHBOARD === "true") { return <NewDashboard />; } ``` **Why it's wrong:** Feature flag logic scattered across codebase, no type safety, hard to discover all flags. **What to do instead:** Centralize feature flags in a single file with type-safe exports. --- ### Scattered Configuration ```typescript // ANTI-PATTERN: Multiple files reading env vars directly // api-client.ts const timeout = Number(process.env.VITE_API_TIMEOUT); // analytics.ts const enabled = process.env.VITE_ENABLE_ANALYTICS === "true"; ``` **Why it's wrong:** Scattered configuration makes it hard to understand app behavior, duplicated parsing logic. **What to do instead:** Create centralized config object with environment-specific overrides. --- ## Quick Reference ### Environment File Checklist - [ ] Per-app .env files (not root-level) - [ ] .env.example maintained with all variables documented - [ ] Comments explain purpose and format - [ ] Secrets in .env.local (gitignored) - [ ] Framework-specific prefixes used correctly - [ ] SCREAMING_SNAKE_CASE naming convention ### Zod Validation Checklist - [ ] All env vars validated at startup - [ ] `z.coerce.number()` used for number types - [ ] `z.stringbool()` used for boolean types (NOT `z.coerce.boolean()`) - [ ] Default values for optional variables - [ ] Clear error messages on validation failure - [ ] Type-safe env object exported - [ ] Consider T3 Env for Next.js/Vite projects (`@t3-oss/env-nextjs`) ### Secret Management Checklist - [ ] .gitignore includes `.env.local` and `.env.*.local` - [ ] No secrets in committed .env files - [ ] CI/CD secrets used for production - [ ] Sensitive file extensions (`*.key`, `*.pem`) gitignored - [ ] Secrets rotated regularly ### Feature Flag Checklist - [ ] Centralized in single file - [ ] Type-safe exports - [ ] Named constant exports - [ ] Code splitting for disabled features - [ ] Environment-specific defaults -
SKILL.md 10 KB
--- name: infra-config-setup-env description: Environment configuration, Zod validation --- # Environment Management > **Quick Guide:** Per-app .env files. Framework-specific prefixes (`NEXT_PUBLIC_*` for Next.js, `VITE_*` for 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 - `NEXT_PUBLIC_*` 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, `NEXT_PUBLIC_` 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 an external feature flag service) - User-specific settings (use database or user preferences) - Frequently changing values (use configuration API or database) - Complex A/B testing with gradual rollouts (use a dedicated feature flag service) **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 (`NEXT_PUBLIC_*` for client, `VITE_*` for Vite client) - .env.example templates for documentation and onboarding **Detailed Resources:** - For code examples, see [examples/](examples/) folder: - [examples/core.md](examples/core.md) - Essential patterns (per-app .env, Zod validation) - [examples/t3-env.md](examples/t3-env.md) - T3 Env pattern for Next.js/Vite (recommended) - [examples/naming-and-templates.md](examples/naming-and-templates.md) - Framework prefixes, .env.example - [examples/security-and-secrets.md](examples/security-and-secrets.md) - Secret management - [examples/feature-flags-and-config.md](examples/feature-flags-and-config.md) - Feature flags, centralized config - For decision frameworks and anti-patterns, see [reference.md](reference.md) --- <philosophy> ## 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. </philosophy> --- <patterns> ## 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 1. **`.env`** - Default development values (committed for apps, gitignored for sensitive packages) 2. **`.env.example`** - Documentation template (committed, shows all required variables) 3. **`.env.local`** - Local developer overrides (gitignored, takes precedence over `.env`) 4. **`.env.production`** - Production configuration (committed or in CI secrets) 5. **`.env.local.example`** - Local override template (committed) #### Loading Order and Precedence **Next.js loading order (highest to lowest priority):** 1. `process.env` (already set in environment) 2. `.env.$(NODE_ENV).local` (e.g., `.env.production.local`) 3. `.env.local` (not loaded when `NODE_ENV=test`) 4. `.env.$(NODE_ENV)` (e.g., `.env.production`) 5. `.env` **Vite loading order:** 1. `.env.[mode].local` (e.g., `.env.production.local`) 2. `.env.[mode]` (e.g., `.env.production`) 3. `.env.local` 4. `.env` **Exception:** Shared variables can go in your build tool's env configuration for cache invalidation See [examples/core.md](examples/core.md) for complete code examples. --- ### Pattern 2: Type-Safe Environment Variables with Zod Validate environment variables at application startup using Zod schemas. Define a schema, parse at startup, export a typed `env` object. ```typescript // lib/env.ts const envSchema = z.object({ VITE_API_URL: z.string().url(), VITE_API_TIMEOUT: z.coerce.number().default(DEFAULT_API_TIMEOUT_MS), VITE_ENABLE_ANALYTICS: z.stringbool().default(false), // Zod 4+ (NOT z.coerce.boolean()) }); export const env = envSchema.parse(import.meta.env); ``` **Key gotchas:** - `z.coerce.boolean()` converts `"false"` to `true` (string is truthy) - always use `z.stringbool()` instead - Use `error.issues` (not `error.errors`) for Zod 4 error handling > **Note:** For Next.js/Vite projects, consider T3 Env (`@t3-oss/env-nextjs` or `@t3-oss/env-core`) for client/server variable separation and build-time validation. See [examples/t3-env.md](examples/t3-env.md). See [examples/core.md](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 1. **SCREAMING_SNAKE_CASE** - All environment variables use uppercase with underscores 2. **Descriptive names** - Variable names clearly indicate purpose 3. **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](examples/naming-and-templates.md) for complete code examples with good/bad comparisons. </patterns> --- <integration> ## Integration Guide **Core dependencies:** - **Zod** (v4+): Runtime validation and type inference for environment variables - **T3 Env** (`@t3-oss/env-nextjs`, `@t3-oss/env-core`): Recommended wrapper for client/server separation **Framework support:** - **Next.js**: Automatic .env file loading with `NEXT_PUBLIC_*` prefix for client-side - **Vite**: Automatic .env file loading with `VITE_*` prefix for client-side **Monorepo considerations:** - Declare shared env vars in your build tool's env configuration for cache invalidation - Use per-app .env files even in monorepos to prevent conflicts **Replaces / Conflicts with:** - Hardcoded configuration values (use env vars instead) - Runtime feature flag services for simple boolean flags (use env vars first, upgrade when needing gradual rollouts) </integration> --- <decision_framework> ## Decision Framework See [reference.md](reference.md) for complete decision frameworks including environment configuration and 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 `NEXT_PUBLIC_*` 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()` (Zod 4+) instead - Empty string env vars are NOT `undefined` - use T3 Env's `emptyStringAsUndefined: true` option - Monorepo build tool caches may NOT be invalidated by env changes unless declared in the tool's env configuration See [reference.md](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 - `NEXT_PUBLIC_*` 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>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.