Claude Skill

infra-config-setup-env

Environment configuration, Zod validation

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_infra-config-setup-env_skills_infra-config-setup-env-3a51ef5.zip · 15 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/infra-config-setup-env/skills/infra-config-setup-env
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git 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:





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

No comments yet.

Reviews (0)

No reviews yet.

Related