# Environment Variable Management

## When to Use This Guide

**Use when:**
- Setting up application configuration
- Managing per-environment settings
- Configuring local development
- Debugging configuration issues
- Setting up CI/CD pipelines

## Core Principles

### 1. Separate Configuration from Code

```python
# BAD: Hardcoded configuration
DATABASE_URL = "postgresql://user:pass@localhost/mydb"
API_KEY = "sk-abc123"

# GOOD: Environment-based configuration
import os
DATABASE_URL = os.getenv("DATABASE_URL")
API_KEY = os.getenv("API_KEY")
```

### 2. Fail Fast on Missing Required Variables

```python
import os
import sys


def get_required_env(key: str) -> str:
    """Get required environment variable or exit.

    Args:
        key: Environment variable name

    Returns:
        Environment variable value

    Raises:
        SystemExit: If variable is not set
    """
    value = os.getenv(key)
    if not value:
        print(f"ERROR: Required environment variable '{key}' is not set")
        print(f"Set it with: export {key}='your-value'")
        sys.exit(1)
    return value


# Usage
DATABASE_URL = get_required_env("DATABASE_URL")
```

### 3. Provide Sensible Defaults for Optional Variables

```python
import os


def get_env(key: str, default: str | None = None) -> str | None:
    """Get environment variable with optional default.

    Args:
        key: Environment variable name
        default: Default value if not set

    Returns:
        Environment variable value or default
    """
    return os.getenv(key, default)


# Usage
LOG_LEVEL = get_env("LOG_LEVEL", "INFO")
CACHE_TTL = int(get_env("CACHE_TTL", "3600"))
DEBUG = get_env("DEBUG", "false").lower() == "true"
```

### 4. Never Commit Secrets

```gitignore
# .gitignore - ALWAYS include these patterns
.env
.env.*
!.env.example
.envrc
.envrc.local
*.pem
*.key
credentials.json
```

## File Organization

### Recommended Structure

```
project/
├── .env.example          # Template with placeholder values (committed)
├── .env                  # Local overrides (gitignored)
├── .envrc                # direnv auto-loading (gitignored)
├── .envrc.example        # direnv template (committed)
└── src/
    └── config.py         # Configuration loading (committed)
```

### .env.example (Template)

```bash
# .env.example - Copy to .env and fill in values
# DO NOT commit .env with real values

# Database
DATABASE_URL=postgresql://user:password@localhost:5432/dbname

# API Keys (get from provider dashboard)
OPENAI_API_KEY=sk-your-key-here
STRIPE_SECRET_KEY=sk_test_your-key-here

# Application Settings
LOG_LEVEL=INFO
DEBUG=false

# Feature Flags
FEATURE_NEW_CHECKOUT=false
FEATURE_DARK_MODE=false
```

### .envrc (direnv Configuration)

```bash
# .envrc - Auto-loaded by direnv
# Install: brew install direnv
# Enable: direnv allow

# Load .env file if it exists
dotenv_if_exists

# Or set variables directly (for secrets, prefer .env)
export LOG_LEVEL=DEBUG
export DEBUG=true

# Activate virtual environment
source_env .venv/bin/activate 2>/dev/null || true
```

## Configuration Loading Patterns

### Pattern 1: Simple Environment Loading

```python
"""Simple configuration from environment variables."""

import os
from dataclasses import dataclass


@dataclass
class Config:
    """Application configuration."""

    # Required
    database_url: str

    # Optional with defaults
    log_level: str = "INFO"
    debug: bool = False
    cache_ttl: int = 3600

    @classmethod
    def from_environment(cls) -> "Config":
        """Load configuration from environment."""
        database_url = os.getenv("DATABASE_URL")
        if not database_url:
            raise ValueError("DATABASE_URL environment variable is required")

        return cls(
            database_url=database_url,
            log_level=os.getenv("LOG_LEVEL", "INFO"),
            debug=os.getenv("DEBUG", "false").lower() == "true",
            cache_ttl=int(os.getenv("CACHE_TTL", "3600")),
        )


# Usage
config = Config.from_environment()
```

### Pattern 2: Pydantic Settings (Recommended for Complex Apps)

```python
"""Configuration with Pydantic for validation."""

from pydantic import Field, PostgresDsn, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    """Application settings with validation.

    Environment variables are automatically loaded.
    Prefix with APP_ to namespace (e.g., APP_DATABASE_URL).
    """

    model_config = SettingsConfigDict(
        env_prefix="APP_",
        env_file=".env",
        env_file_encoding="utf-8",
    )

    # Required - no default
    database_url: PostgresDsn

    # Required secrets
    api_key: str = Field(..., description="API key for external service")

    # Optional with defaults
    log_level: str = Field(default="INFO", pattern="^(DEBUG|INFO|WARNING|ERROR)$")
    debug: bool = False
    cache_ttl: int = Field(default=3600, ge=0, le=86400)

    # Computed fields
    @field_validator("log_level", mode="before")
    @classmethod
    def uppercase_log_level(cls, v: str) -> str:
        return v.upper() if v else "INFO"


# Usage (auto-loads from environment)
settings = Settings()
print(settings.database_url)
```

### Pattern 3: Environment-Specific Configuration

```python
"""Per-environment configuration loading."""

import os
from dataclasses import dataclass
from typing import Literal


Environment = Literal["development", "staging", "production"]


@dataclass
class EnvironmentConfig:
    """Environment-specific configuration."""

    environment: Environment
    debug: bool
    log_level: str
    database_pool_size: int
    enable_caching: bool


# Environment-specific defaults
ENVIRONMENT_DEFAULTS: dict[Environment, EnvironmentConfig] = {
    "development": EnvironmentConfig(
        environment="development",
        debug=True,
        log_level="DEBUG",
        database_pool_size=5,
        enable_caching=False,
    ),
    "staging": EnvironmentConfig(
        environment="staging",
        debug=False,
        log_level="INFO",
        database_pool_size=10,
        enable_caching=True,
    ),
    "production": EnvironmentConfig(
        environment="production",
        debug=False,
        log_level="WARNING",
        database_pool_size=20,
        enable_caching=True,
    ),
}


def get_config() -> EnvironmentConfig:
    """Get configuration for current environment."""
    env = os.getenv("ENVIRONMENT", "development")
    if env not in ENVIRONMENT_DEFAULTS:
        raise ValueError(f"Unknown environment: {env}")
    return ENVIRONMENT_DEFAULTS[env]  # type: ignore


config = get_config()
```

## Platform Integration

### Vercel

```bash
# Set via CLI
vercel env add DATABASE_URL production
vercel env add DATABASE_URL preview
vercel env add DATABASE_URL development

# Or via Pulumi
```

```python
import pulumi_vercel as vercel

vercel.ProjectEnvironmentVariable(
    "database-url",
    project_id=project.id,
    key="DATABASE_URL",
    value=database_url,
    target=["production", "preview", "development"],
)
```

### Supabase Edge Functions

```bash
# Set secrets for Edge Functions
supabase secrets set MY_API_KEY=value
supabase secrets list
```

### Cloudflare Workers

```bash
# Set via wrangler
wrangler secret put API_KEY

# Or in wrangler.toml (non-secrets only)
[vars]
ENVIRONMENT = "production"
```

### GitHub Actions

```yaml
# .github/workflows/deploy.yml
env:
  # Non-sensitive
  NODE_ENV: production

jobs:
  deploy:
    steps:
      - name: Deploy
        env:
          # From GitHub Secrets
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          API_KEY: ${{ secrets.API_KEY }}
        run: ./deploy.sh
```

## Common Variables by Category

### Database

```bash
DATABASE_URL=postgresql://user:pass@host:5432/dbname
DATABASE_POOL_SIZE=10
DATABASE_SSL_MODE=require
REDIS_URL=redis://localhost:6379/0
```

### Authentication

```bash
JWT_SECRET=your-secret-key
SESSION_SECRET=your-session-secret
OAUTH_CLIENT_ID=your-client-id
OAUTH_CLIENT_SECRET=your-client-secret
```

### External Services

```bash
# AI/ML
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...

# Payments
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Email
SENDGRID_API_KEY=SG....
SMTP_HOST=smtp.example.com
```

### Application Settings

```bash
# Basics
ENVIRONMENT=production
LOG_LEVEL=INFO
DEBUG=false

# URLs
APP_URL=https://myapp.com
API_URL=https://api.myapp.com

# Feature Flags
FEATURE_NEW_UI=true
FEATURE_BETA_API=false
```

## Troubleshooting

### Variable Not Loading

```bash
# Check if variable is set
echo $DATABASE_URL

# Check in Python
python -c "import os; print(os.getenv('DATABASE_URL'))"

# Check direnv status
direnv status
```

### Common Issues

| Issue | Cause | Solution |
|-------|-------|----------|
| Variable is `None` | Not exported | Use `export VAR=value` |
| Variable not updating | Cached value | Restart shell/process |
| direnv not loading | Not allowed | Run `direnv allow` |
| .env not loading | Wrong path | Check working directory |

## Best Practices Checklist

- [ ] `.env` is in `.gitignore`
- [ ] `.env.example` exists with all variables (placeholder values)
- [ ] Required variables fail fast if missing
- [ ] Optional variables have sensible defaults
- [ ] Secrets are never logged or printed
- [ ] Configuration is validated at startup
- [ ] Documentation explains each variable

## Related Documentation

- **Configuration as Code**: [configuration-as-code.md](./configuration-as-code.md) - Philosophy
- **Secrets Management**: [secrets-management.md](./secrets-management.md) - Handling secrets
- **Environment Isolation**: [environment-isolation.md](./environment-isolation.md) - Per-env setup
- **Vercel**: [vercel.md](./vercel.md) - Vercel env vars
- **Supabase**: [supabase.md](./supabase.md) - Supabase secrets

---

**Key Takeaway**: Environment variables separate configuration from code, enabling the same codebase to run in different environments. Always fail fast on missing required variables, never commit secrets, and use `.env.example` to document all configuration options.
