Back to catalog

env-management

Skill from vincentbollaert/turborepo-monorepo

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • GitHub stars: 2
  • Updated: 2025-11-21

Download the latest version (v1.0.0) from this page.

SKILL.md

# Environment Management

**Auto-detection:** Environment variables, .env files, Zod validation, secrets management, NEXT*PUBLIC* prefix, VITE\_ prefix, feature flags

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

**Key patterns covered:**

- Per-app .env files (not root-level, prevents conflicts)
- Zod validation at startup for type safety and early failure
- Framework-specific prefixes (NEXT*PUBLIC*\_ for client, VITE\_\_ for Vite client)
- .env.example templates for documentation and onboarding

---

# Environment Management

> **Quick Guide:** Per-app .env files (apps/client-next/.env). Framework-specific prefixes (NEXT*PUBLIC*\_, VITE\_\_). Zod validation at startup. Maintain .env.example templates. Never commit secrets (.gitignore). Environment-based feature flags.

**ACTUAL IMPLEMENTATION: Per-app `.env` files with Zod validation**

---

## File Hierarchy

**ACTUAL PATTERN: Per-app environment files**

**Location:** Each app/package has its own `.env` file

### File Structure

**Actual files in this codebase:**

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

**File types:**

1. **`.env`** - Default development values (committed for apps, gitignored for sensitive packages)

   - Contains non-sensitive defaults
   - Used for local development
   - Framework-specific prefixes for client-side variables

2. **`.env.example`** - Documentation template (committed)

   - Shows all required variables (no values)
   - Includes comments explaining each variable
   - Used for onboarding new developers

3. **`.env.local`** - Local developer overrides (gitignored)

   - Personal development settings
   - Never committed
   - Takes precedence over `.env`

4. **`.env.production`** - Production configuration (committed or in CI secrets)

   - Production-specific values (URLs, feature flags)
   - Sensitive values stored in CI secrets
   - Loaded during production builds

5. **`.env.local.example`** - Local override template (committed)
   - Shows example local overrides
   - Helps developers configure local environment

### Loading Order and Precedence

**Next.js loading order (highest to lowest priority):**

1. `.env.$(NODE_ENV).local` (e.g., `.env.production.local`)
2. `.env.local` (not loaded when `NODE_ENV=test`)
3. `.env.$(NODE_ENV)` (e.g., `.env.production`)
4. `.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`

**Best practice:**

- Use `.env` for defaults (committed)
- Use `.env.local` for personal overrides (gitignored)
- Use `.env.production` for production config (committed or CI secrets)
- Never commit secrets (use CI secrets or secret management service)

### Per-App vs Root Environment Files

**Pattern: Per-app environment files (ACTUAL IMPLEMENTATION)**

Per-app prevents conflicts, clarifies ownership. Root-level causes shared variables and larger blast radius.

**Exception:** Shared variables can go in `turbo.json` `env` array

---

## Naming Conventions

**ACTUAL IMPLEMENTATION: Framework-specific prefixes**

### Variable Naming Rules

**Mandatory conventions:**

1. **SCREAMING_SNAKE_CASE** - All environment variables must use uppercase with underscores

   ```bash
   # ✅ GOOD
   NEXT_PUBLIC_API_URL=https://api.example.com
   DATABASE_URL=postgresql://localhost:5432/mydb

   # ❌ BAD
   apiUrl=https://api.example.com
   database_url=postgresql://localhost:5432/mydb
   ```

2. **Descriptive names** - Variable names should clearly indicate purpose

   ```bash
   # ✅ GOOD
   NEXT_PUBLIC_API_URL=https://api.example.com
   NEXT_PUBLIC_API_TIMEOUT_MS=30000

   # ❌ BAD
   URL=https://api.example.com
   TIMEOUT=30000
   ```

3. **Consistent prefixes** - Use framework prefixes for client-side variables

   ```bash
   # Next.js - NEXT_PUBLIC_*
   NEXT_PUBLIC_API_URL=https://api.example.com
   NEXT_PUBLIC_ANALYTICS_ID=UA-123456

   # Vite - VITE_*
   VITE_API_URL=https://api.example.com
   VITE_ANALYTICS_ID=UA-123456
   ```

### Framework-Specific Prefixes

**Next.js variables:**

- **`NEXT_PUBLIC_*`** - Client-side accessible (embedded in bundle)

  - Use for: API URLs, public keys, feature flags
  - Avoid: Secrets, API keys, tokens

- **No prefix** - Server-side only
  - Use for: Database URLs, secret keys, API tokens
  - Never exposed to client

**Example:**

```bash
# Client-side (embedded in JavaScript bundle)
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=UA-123456

# Server-side only (not embedded in bundle)
DATABASE_URL=postgresql://localhost:5432/mydb
API_SECRET_KEY=super-secret-key
```

**Vite variables:**

- **`VITE_*`** - Client-side accessible (embedded in bundle)

  - Use for: API URLs, public configuration
  - Avoid: Secrets

- **No prefix** - Build-time only (not exposed)
  - Use for: Build configuration, secrets

**Node.js/Server variables:**

- **`NODE_ENV`** - Standard Node.js environment (`development`, `production`, `test`)
- **`PORT`** - Server port number
- **No prefix** - All variables available server-side

### Grouping by Purpose

**Pattern: Use consistent prefixes for related variables**

```bash
# API configuration
API_BASE_URL=https://api.example.com
API_TIMEOUT_MS=30000
API_RETRY_ATTEMPTS=3

# Database configuration
DATABASE_URL=postgresql://localhost:5432/mydb
DATABASE_POOL_SIZE=10
DATABASE_SSL_MODE=require

# Feature flags
FEATURE_NEW_DASHBOARD=true
FEATURE_BETA_EDITOR=false
FEATURE_AB_TEST_CHECKOUT=true

# Third-party services
STRIPE_PUBLIC_KEY=pk_test_123
STRIPE_SECRET_KEY=sk_test_456
SENTRY_DSN=https://...
```

**Benefits:**

- Easier to find related variables
- Clear separation of concerns
- Easier to document

---

## Type-Safe Environment Variables

**ACTUAL IMPLEMENTATION: Zod validation (apps/client-next/app/env.ts)**

### Validation Pattern

**Pattern: Validate environment variables at startup with Zod**

**Why validate:**

- Catch missing variables early (at startup, not runtime)
- Enforce correct types (URL, number, boolean, enum)
- Provide helpful error messages
- Single source of truth for environment schema

**Benefits:**

- ✅ Type safety (TypeScript knows variable types)
- ✅ Runtime validation (fails fast with clear errors)
- ✅ Default values (optional variables with defaults)
- ✅ Format validation (URLs must be valid URLs)

### Validation Location

**Where to validate:**

1. **Client-side variables** - Validate in `app/env.ts` or `src/lib/env.ts`
2. **Server-side variables** - Validate in server entry point
3. **Shared variables** - Validate in shared package

**When to validate:**

- ✅ At app startup (before rendering)
- ✅ In provider setup (before API client initialization)
- ❌ Not during import (can cause circular dependencies)

### Error Handling

**Pattern: Fail fast with helpful errors**

```typescript
// ✅ GOOD: Clear error message, fail fast
try {
  return schema.parse(env);
} catch (error) {
  if (error instanceof z.ZodError) {
    console.error("❌ Invalid environment variables:");
    error.errors.forEach((err) => {
      console.error(`  - ${err.path.join(".")}: ${err.message}`);
    });
    throw new Error("Invalid environment configuration");
  }
  throw error;
}

// ❌ BAD: Silent failure or unclear error
return schema.parse(env); // Throws cryptic Zod error
```

**Benefits:**

- Clear error messages for developers
- Identifies exactly which variable is invalid
- Suggests correct format

---

## Environment File Templates

**RECOMMENDED: Maintain `.env.example` for documentation**

### Template Structure

**Pattern: Document all variables with comments**

```bash
# .env.example

# ===================
# API Configuration
# ===================

# Base URL for API requests (required)
# Development: http://localhost:3000/api/v1
# Production: https://api.example.com/api/v1
NEXT_PUBLIC_API_URL=

# API request timeout in milliseconds (optional, default: 30000)
NEXT_PUBLIC_API_TIMEOUT_MS=30000

# ===================
# Feature Flags
# ===================

# Enable analytics tracking (optional, default: false)
NEXT_PUBLIC_ENABLE_ANALYTICS=false

# Enable error tracking with Sentry (optional, default: false)
NEXT_PUBLIC_ENABLE_ERROR_TRACKING=false

# ===================
# Third-Party Services
# ===================

# Google Analytics tracking ID (optional)
# Get from: https://analytics.google.com
NEXT_PUBLIC_GA_TRACKING_ID=

# Sentry DSN for error tracking (optional)
# Get from: https://sentry.io/settings/projects/
SENTRY_DSN=
```

**Template best practices:**

- Group related variables
- Include comments explaining purpose
- Show example values
- Indicate required vs optional
- Link to documentation for third-party services
- No actual values (just placeholders)

### Onboarding Process

**Recommended onboarding steps:**

1. **Clone repository**
2. **Copy `.env.example` to `.env`**
   ```bash
   cp .env.example .env
   ```
3. **Fill in required values**
   - API URLs (usually localhost for development)
   - Service keys (get from team or create new)
4. **Run application**
   - Validation will catch missing required variables

**Documentation:**

- README should mention environment setup
- Link to `.env.example` file
- Document where to get third-party service keys
- Include troubleshooting guide

---

## Security Best Practices

**CRITICAL: Never commit secrets to repository**

### Secret Management

**What are secrets:**

- API keys, tokens, passwords
- Database credentials
- Private keys, certificates
- OAuth client secrets
- Encryption keys

**What NOT to commit:**

- ❌ `.env.local` (local secrets)
- ❌ `.env.production` with real secrets
- ❌ Any file containing actual credentials

**What to commit:**

- ✅ `.env` with non-sensitive defaults
- ✅ `.env.example` with placeholders
- ✅ `.env.production` with variable names only (values in CI)

### .gitignore Configuration

**Recommended .gitignore:**

```gitignore
# Environment files
.env.local
.env.*.local

# Optional: ignore all .env files except example
# .env
# !.env.example

# Sensitive files
*.key
*.pem
*.p12
*.pfx
```

### CI/CD Secret Management

**Pattern: Store secrets in CI/CD platform**

**GitHub Actions:**

- Use GitHub Secrets (Settings → Secrets and variables → Actions)
- Access via `${{ secrets.SECRET_NAME }}`
- Secrets are encrypted and masked in logs

**Vercel:**

- Use Vercel Environment Variables (Project Settings → Environment Variables)
- Separate values per environment (production, preview, development)
- Access via CLI: `vercel env add VARIABLE_NAME production`

**Best practices:**

- Use different secrets per environment (staging vs production)
- Rotate secrets regularly (quarterly minimum)
- Use least-privilege access (separate secrets for each service)
- Never share secrets in chat or email
- Use secret scanning (GitHub Advanced Security, TruffleHog)

### Secret Rotation

**Recommended rotation schedule:**

- **Critical secrets** - Quarterly or on team member departure
- **API keys** - Annually or on suspicious activity
- **Passwords** - Every 90 days (or use OAuth)

**Rotation process:**

1. Generate new secret
2. Add to CI/CD platform
3. Deploy with new secret
4. Monitor for issues
5. Revoke old secret

### Turbo Environment Variable Validation

**ACTUAL IMPLEMENTATION: Turborepo plugin validates env vars**

**Pattern: Declare environment variables in turbo.json**

```json
{
  "tasks": {
    "build": {
      "env": ["NEXT_PUBLIC_API_URL", "NODE_ENV", "DATABASE_URL"]
    }
  }
}
```

**ESLint validation:**

```javascript
// packages/eslint-config/base.js
{
  plugins: {
    turbo: turboPlugin,
  },
  rules: {
    "turbo/no-undeclared-env-vars": "warn",
  },
}
```

**Why:**

- Catches undeclared environment variables at lint time
- Ensures variables are properly declared in turbo.json
- Prevents accidental reliance on undeclared variables

---

## Feature Flags

**RECOMMENDED: Environment-based feature flags**

### Simple Feature Flags

**Pattern: Boolean environment variables**

```bash
# .env
NEXT_PUBLIC_FEATURE_NEW_DASHBOARD=false
NEXT_PUBLIC_FEATURE_BETA_EDITOR=false
NEXT_PUBLIC_FEATURE_AB_TEST=true
```

**Usage:**

```typescript
const FEATURES = {
  NEW_DASHBOARD: process.env.NEXT_PUBLIC_FEATURE_NEW_DASHBOARD === 'true',
  BETA_EDITOR: process.env.NEXT_PUBLIC_FEATURE_BETA_EDITOR === 'true',
  AB_TEST: process.env.NEXT_PUBLIC_FEATURE_AB_TEST === 'true',
} as const;

// Use in components
if (FEATURES.NEW_DASHBOARD) {
  return <NewDashboard />;
}
```

**Benefits:**

- Simple to implement
- No external dependencies
- Works offline
- Fast (no API calls)

**Limitations:**

- Requires rebuild to change flags
- No gradual rollouts (0% or 100%)
- No user targeting
- No runtime toggling

### Advanced Feature Flags

**When to use external service:**

Use services like LaunchDarkly, PostHog, or Unleash when you need:

- Runtime flag toggling (no deploys)
- Gradual rollouts (5% → 25% → 50% → 100%)
- User targeting (beta users, specific companies)
- A/B testing with analytics
- Scheduled releases

**Trade-offs:**

- ✅ No deploys needed to toggle flags
- ✅ Gradual rollouts reduce risk
- ✅ User targeting for beta testing
- ❌ Additional dependency
- ❌ Requires internet connection
- ❌ Cost (most services are paid)

### Feature Flag Hygiene

**Best practices:**

- Clean up old flags (remove after full rollout)
- Document flag dependencies (flag A requires flag B)
- Test both enabled and disabled states
- Use TypeScript for type-safe flags
- Monitor flag usage (which flags are actively used)

**Anti-patterns:**

- ❌ Too many nested flags (complex logic)
- ❌ Flags that never get removed (technical debt)
- ❌ Flags without documentation
- ❌ Flags that depend on each other (complex dependencies)

---

## Configuration by Environment

**RECOMMENDED: Environment-specific configuration object**

### Configuration Pattern

**Pattern: Single configuration object with environment overrides**

**Why:**

- Centralized configuration
- Type-safe access
- Easy to test
- Clear environment differences

**Structure:**

1. Base configuration (shared across environments)
2. Environment-specific overrides (development, staging, production)
3. Merge into single config object

### Configuration Categories

**Common configuration categories:**

1. **API Configuration**

   - Base URL
   - Timeout
   - Retry attempts
   - Rate limits

2. **Cache Configuration**

   - TTL (time to live)
   - Max size
   - Eviction policy

3. **Feature Configuration**

   - Feature flags
   - Analytics
   - Error tracking

4. **Performance Configuration**
   - Bundle size limits
   - Image optimization
   - Lazy loading thresholds

### Environment Detection

**Pattern: Detect environment from env variable**

```typescript
const ENV = process.env.NODE_ENV || "development";
const IS_PRODUCTION = ENV === "production";
const IS_DEVELOPMENT = ENV === "development";
const IS_TEST = ENV === "test";
```

**Next.js environments:**

- `development` - Local development (`next dev`)
- `production` - Production build (`next build` + `next start`)
- `test` - Testing environment (Vitest, Jest)

---

## Common Anti-Patterns

**❌ NEVER do these:**

- Committing secrets to repository (use .gitignore)
- Using `process.env.VARIABLE` directly without validation
- Hardcoding environment values in code
- Sharing `.env` files via Slack/email
- Using production secrets in development
- Not documenting variables in `.env.example`
- Skipping validation (missing variables cause runtime errors)
- Using different variable names across environments
- Not rotating secrets
- Exposing secrets in client-side code (must use `NEXT_PUBLIC_*` prefix for client-side)

---

## Resources

**Official documentation:**

- Next.js Environment Variables: https://nextjs.org/docs/app/building-your-application/configuring/environment-variables
- Vite Environment Variables: https://vitejs.dev/guide/env-and-mode.html
- Turborepo Environment Variables: https://turbo.build/repo/docs/handbook/environment-variables

**Tools:**

- Zod validation: https://zod.dev/
- dotenv: https://github.com/motdotla/dotenv
- TruffleHog (secret scanning): https://github.com/trufflesecurity/trufflehog

**Feature flags:**

- LaunchDarkly: https://launchdarkly.com/
- PostHog: https://posthog.com/
- Unleash: https://www.getunleash.io/


---

# Environment Management - Examples

---

## File Hierarchy

### Example: ACTUAL .env Files

**apps/client-next/.env:**

```bash
NEXT_PUBLIC_API_URL=http://localhost:3000/api/v1
```

**apps/server/.env.example:**

```bash
# base configuration
NODE_ENV=development
PORT=1337
```

**Why:** Per-app configuration. Clear defaults. Template for new developers.

---

### Example: Multiple Environment Files with Loading Order

```
# Project structure
apps/client-react/
├── .env                    # Default values (committed)
├── .env.local              # Local overrides (gitignored)
├── .env.development        # Development defaults
├── .env.staging            # Staging configuration
├── .env.production         # Production configuration
└── .env.example            # Documentation template
```

**Contents:**

```bash
# .env - Default values (committed)
VITE_API_URL=http://localhost:8000
VITE_API_TIMEOUT=30000
VITE_ENABLE_ANALYTICS=false
```

```bash
# .env.local - Local overrides (gitignored)
VITE_API_URL=http://localhost:3001
VITE_ENABLE_ANALYTICS=true
```

```bash
# .env.development - Development defaults
VITE_ENVIRONMENT=development
VITE_API_URL=https://dev-api.example.com
VITE_ENABLE_ERROR_TRACKING=false
```

```bash
# .env.staging - Staging configuration
VITE_ENVIRONMENT=staging
VITE_API_URL=https://staging-api.example.com
VITE_ENABLE_ANALYTICS=true
VITE_ENABLE_ERROR_TRACKING=true
```

```bash
# .env.production - Production configuration
VITE_ENVIRONMENT=production
VITE_API_URL=https://api.example.com
VITE_ENABLE_ANALYTICS=true
VITE_ENABLE_ERROR_TRACKING=true
```

```gitignore
# .gitignore
.env.local
.env.*.local
```

**Why:** Environment-specific configs. Local development flexibility. Secure defaults. Clear separation.

**Edge Cases:**
- Load priority: .env.{mode}.local > .env.{mode} > .env.local > .env
- Never commit secrets
- Document all variables in .env.example

---

### Example: Vite Type-Safe Environment Variables

```typescript
// lib/env.ts
import { z } from "zod";

const envSchema = z.object({
  // Public variables (VITE_ prefix)
  VITE_API_URL: z.string().url(),
  VITE_API_TIMEOUT: z.coerce.number().default(30000),
  VITE_ENABLE_ANALYTICS: z.coerce.boolean().default(false),
  VITE_ENVIRONMENT: z.enum(["development", "staging", "production"]),

  // Build-time variables
  MODE: z.enum(["development", "production"]),
  DEV: z.boolean(),
  PROD: z.boolean(),
});

// Validate and export
function validateEnv() {
  try {
    return envSchema.parse(import.meta.env);
  } catch (error) {
    if (error instanceof z.ZodError) {
      console.error("❌ Invalid environment variables:");
      error.errors.forEach((err) => {
        console.error(`  - ${err.path.join(".")}: ${err.message}`);
      });
      throw new Error("Invalid environment configuration");
    }
    throw error;
  }
}

export const env = validateEnv();

// Type-safe usage
console.log(env.VITE_AP