Skip to main content

ADR-046: Environment Configuration

Status

Implemented

Date

2025-01-16 (Retrospective)

Decision Makers

  • DevOps Team - Configuration management
  • Architecture Team - 12-Factor compliance

Layer

Infrastructure

  • ADR-025: 12-Factor App Compliance

Supersedes

None

Depends On

None

Context

Application configuration varies by environment:

  1. Development: Local settings, debug enabled
  2. Staging: Production-like, test data
  3. Production: Secure, optimized settings
  4. Secrets: API keys, passwords
  5. Feature Flags: Environment-specific features

Requirements:

  • Environment variables as primary mechanism
  • .env files for development
  • Secret management integration
  • Validation at startup
  • Type-safe configuration

Decision

We implement environment-based configuration with Pydantic Settings:

Key Design Decisions

  1. Environment Variables: Primary configuration source
  2. Pydantic Settings: Type-safe config with validation
  3. .env Files: Development convenience
  4. Secret References: Support for secret managers
  5. Environment Prefix: Optional VITE_ / OPS_ prefixes

Backend Configuration

# backend/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

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

# Database
database_url: str
database_pool_size: int = 10

# Redis
redis_url: str = "redis://localhost:6379/0"

# Security
secret_key: str
jwt_secret: str | None = None
jwt_algorithm: str = "HS256"

# Environment
environment: str = "development"
debug: bool = False
log_level: str = "INFO"

# Features
auth_provider: str = "local"
auth_dev_override: bool = False

# AI/ML
openai_api_key: str | None = None
embedding_model: str = "text-embedding-ada-002"

model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)

@property
def jwt_secret_key(self) -> str:
return self.jwt_secret or self.secret_key

settings = Settings()

Frontend Configuration

// frontend/src/config/environment.ts
export const config = {
apiUrl: import.meta.env.VITE_API_URL || 'http://localhost:8888',
environment: import.meta.env.VITE_ENVIRONMENT || 'development',
sentryDsn: import.meta.env.VITE_SENTRY_DSN,
features: {
darkMode: import.meta.env.VITE_FEATURE_DARK_MODE === 'true',
analytics: import.meta.env.VITE_FEATURE_ANALYTICS === 'true',
},
};

Environment Files

# .env.example (committed)
DATABASE_URL=postgresql://user:pass@localhost:5432/ops
REDIS_URL=redis://localhost:6379/0
SECRET_KEY=change-me-in-production
ENVIRONMENT=development
DEBUG=true

# .env (not committed)
DATABASE_URL=postgresql://ops:secret@localhost:5432/ops
SECRET_KEY=actual-secret-key
OPENAI_API_KEY=sk-...

Environment-Specific Behavior

class Settings(BaseSettings):
@property
def is_production(self) -> bool:
return self.environment == "production"

@property
def is_development(self) -> bool:
return self.environment == "development"

def get_cors_origins(self) -> list[str]:
if self.is_production:
return ["https://ops.example.com"]
return ["http://localhost:3000", "http://localhost:5173"]

Consequences

Positive

  • 12-Factor Compliant: Config from environment
  • Type Safety: Pydantic validates at startup
  • Secret Separation: .env not committed
  • Easy Override: Environment vars override defaults
  • Documentation: Settings class documents all options

Negative

  • Environment Sprawl: Many variables to manage
  • Secret Management: Need external secret manager for prod
  • Startup Failure: Missing required vars fail startup
  • IDE Support: Less autocomplete for env vars

Neutral

  • Default Values: Balance convenience vs explicit config
  • Prefix Strategy: VITE_ for frontend, optional for backend

Implementation Status

  • Core implementation complete
  • Tests written and passing
  • Documentation updated
  • Migration/upgrade path defined
  • Monitoring/observability in place

Implementation Details

  • Backend Config: backend/core/config.py
  • Frontend Config: frontend/src/config/environment.ts
  • Example: .env.example
  • Docker: docker-compose.yml (environment section)

LLM Council Review

Review Date: 2025-01-16 Confidence Level: High (100%) Verdict: CONDITIONAL ACCEPTANCE

Quality Metrics

  • Consensus Strength Score (CSS): 0.88
  • Deliberation Depth Index (DDI): 0.85

Council Feedback Summary

Strong 12-Factor compliant foundation. However, significant gaps in secret safety (logging risk), type validation, and frontend build lifecycle need to be addressed.

Key Concerns Identified:

  1. Secret Logging Risk: Plain str for secrets appears in tracebacks/logs
  2. Frontend Build-Time Baking: Vite's import.meta.env replaces at build → can't promote image across environments
  3. Missing URL Validation: Database/Redis URLs should use strict types
  4. Loose Environment Type: Free text environment allows typos

Required Modifications:

  1. Use SecretStr for Secrets:
    secret_key: SecretStr
    openai_api_key: SecretStr | None = None
  2. Use Strict URL Types:
    database_url: PostgresDsn
    redis_url: RedisDsn
  3. Constrain Environment:
    environment: Literal["development", "staging", "production"]
  4. Frontend Runtime Config:
    • Inject config.js into window object at container start
    • Or document the "rebuild per environment" trade-off
  5. Frontend Validation: Add Zod schema to fail fast on missing required config
  6. Secret Resolution: Document how to resolve sm:// prefixed values from secret managers
  7. Production Override Protection: Ensure env_file doesn't override K8s injected vars in prod

Modifications Applied

  1. Documented SecretStr usage for sensitive fields
  2. Added strict URL type requirements
  3. Documented environment literal constraint
  4. Added frontend runtime config recommendation
  5. Documented production env var protection

Council Ranking

  • gpt-5.2: Best Response (secret safety)
  • gemini-3-pro: Strong (frontend lifecycle)
  • claude-opus-4.5: Good (type validation)

References


ADR-046 | Infrastructure Layer | Implemented