ADR-046: Environment Configuration
Status
Implemented
Date
2025-01-16 (Retrospective)
Decision Makers
- DevOps Team - Configuration management
- Architecture Team - 12-Factor compliance
Layer
Infrastructure
Related ADRs
- ADR-025: 12-Factor App Compliance
Supersedes
None
Depends On
None
Context
Application configuration varies by environment:
- Development: Local settings, debug enabled
- Staging: Production-like, test data
- Production: Secure, optimized settings
- Secrets: API keys, passwords
- 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
- Environment Variables: Primary configuration source
- Pydantic Settings: Type-safe config with validation
- .env Files: Development convenience
- Secret References: Support for secret managers
- 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:
- Secret Logging Risk: Plain
strfor secrets appears in tracebacks/logs - Frontend Build-Time Baking: Vite's
import.meta.envreplaces at build → can't promote image across environments - Missing URL Validation: Database/Redis URLs should use strict types
- Loose Environment Type: Free text
environmentallows typos
Required Modifications:
- Use SecretStr for Secrets:
secret_key: SecretStr
openai_api_key: SecretStr | None = None - Use Strict URL Types:
database_url: PostgresDsn
redis_url: RedisDsn - Constrain Environment:
environment: Literal["development", "staging", "production"] - Frontend Runtime Config:
- Inject
config.jsinto window object at container start - Or document the "rebuild per environment" trade-off
- Inject
- Frontend Validation: Add Zod schema to fail fast on missing required config
- Secret Resolution: Document how to resolve
sm://prefixed values from secret managers - Production Override Protection: Ensure env_file doesn't override K8s injected vars in prod
Modifications Applied
- Documented SecretStr usage for sensitive fields
- Added strict URL type requirements
- Documented environment literal constraint
- Added frontend runtime config recommendation
- 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