Files
orion/docs/development/environment-detection.md

1116 lines
27 KiB
Markdown

# Environment Detection System
**Version:** 1.0
**Last Updated:** November 2025
**Audience:** Development Team
---
## Table of Contents
1. [Overview](#overview)
2. [How It Works](#how-it-works)
3. [Using Environment Detection](#using-environment-detection)
4. [Configuration](#configuration)
5. [Development Guidelines](#development-guidelines)
6. [Deployment Guide](#deployment-guide)
7. [Testing](#testing)
8. [Best Practices](#best-practices)
---
## Overview
The Wizamart platform uses **automatic environment detection** to determine the runtime environment (development, staging, or production) and adjust security settings accordingly.
### Why Auto-Detection?
Instead of manually configuring environment settings in code, the system automatically detects the environment based on:
- Environment variables
- Runtime indicators (hostname, debug mode, etc.)
- Safe defaults for local development
This approach:
- ✅ Works out of the box in development (no configuration needed)
- ✅ Reduces configuration errors
- ✅ Follows "convention over configuration" principle
- ✅ Simplifies deployment process
### Key Use Cases
The environment detection system is primarily used to determine:
1. **Cookie Security** - Should cookies require HTTPS? (`secure` flag)
2. **Debug Mode** - Enable detailed error messages in development
3. **CORS Settings** - Stricter in production, relaxed in development
4. **Logging Level** - Verbose in development, structured in production
---
## How It Works
### Detection Logic
The system determines the environment using a priority-based approach:
```
1. Check ENV environment variable
↓ (if not set)
2. Check ENVIRONMENT environment variable
↓ (if not set)
3. Auto-detect from system indicators
↓ (if no indicators)
4. Default to "development" (safe for local work)
```
### Detection Details
#### Priority 1: ENV Variable
```bash
ENV=production
ENV=staging
ENV=development # or "dev" or "local"
```
#### Priority 2: ENVIRONMENT Variable
```bash
ENVIRONMENT=production
ENVIRONMENT=staging
ENVIRONMENT=development
```
#### Priority 3: Auto-Detection
The system checks for indicators:
**Development Indicators:**
- `DEBUG=true` environment variable
- Hostname contains: "local", "dev", "laptop", "desktop"
- Running on: localhost, 127.0.0.1
**Staging Indicators:**
- Hostname contains: "staging", "stage"
**Production Indicators:**
- None of the above (explicit production setting recommended)
#### Priority 4: Safe Default
If no environment is detected: **defaults to development**
- Safe for local development
- Cookies work with HTTP
- Detailed error messages enabled
---
## Using Environment Detection
### Module Location
```python
app/core/environment.py
```
### Available Functions
```python
from app.core.environment import (
get_environment, # Returns: "development" | "staging" | "production"
is_development, # Returns: bool
is_staging, # Returns: bool
is_production, # Returns: bool
should_use_secure_cookies, # Returns: bool
get_cached_environment, # Returns: cached environment (performance)
)
```
### Common Usage Patterns
#### 1. Cookie Security Settings
```python
from fastapi import Response
from app.core.environment import should_use_secure_cookies
@router.post("/api/v1/admin/auth/login")
def login(response: Response):
# Set authentication cookie
response.set_cookie(
key="admin_token",
value=token,
httponly=True,
secure=should_use_secure_cookies(), # Auto-detects environment
samesite="lax",
path="/admin"
)
return {"message": "Logged in"}
```
**Behavior:**
- **Development** → `secure=False` (works with HTTP)
- **Production** → `secure=True` (requires HTTPS)
#### 2. Conditional Features
```python
from app.core.environment import is_development, is_production
@router.get("/api/v1/debug/info")
def debug_info():
if not is_development():
raise HTTPException(status_code=404, detail="Not found")
# Only accessible in development
return {
"database": get_db_info(),
"cache": get_cache_stats(),
"config": get_config_dump()
}
```
#### 3. Error Messages
```python
from app.core.environment import is_development
@app.exception_handler(Exception)
async def exception_handler(request: Request, exc: Exception):
if is_development():
# Detailed error in development
return JSONResponse(
status_code=500,
content={
"error": str(exc),
"traceback": traceback.format_exc(),
"request": str(request.url)
}
)
else:
# Generic error in production
return JSONResponse(
status_code=500,
content={"error": "Internal server error"}
)
```
#### 4. CORS Configuration
```python
from fastapi.middleware.cors import CORSMiddleware
from app.core.environment import is_development
if is_development():
# Relaxed CORS for development
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
else:
# Strict CORS for production
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourdomain.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
)
```
#### 5. Logging Configuration
```python
import logging
from app.core.environment import get_environment
def configure_logging():
env = get_environment()
if env == "development":
# Verbose logging for development
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
elif env == "staging":
# Moderate logging for staging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
else: # production
# Structured logging for production
logging.basicConfig(
level=logging.WARNING,
format='{"timestamp":"%(asctime)s","level":"%(levelname)s","message":"%(message)s"}'
)
```
#### 6. Performance Optimization
```python
from app.core.environment import get_cached_environment
def process_request(request: Request):
# Use cached version for frequent calls
env = get_cached_environment() # Cached - faster
# Instead of:
# env = get_environment() # Re-detects every time
if env == "production":
# Production-specific optimization
use_cache = True
else:
use_cache = False
```
---
## Configuration
### Development (Default)
**No configuration needed!** The system auto-detects development mode.
**Indicators:**
- Running on localhost or 127.0.0.1
- No environment variables set
- DEBUG mode enabled
**Settings:**
- `should_use_secure_cookies()``False`
- Cookies work with HTTP (http://localhost:8000)
- Detailed error messages
### Staging Environment
**Set environment variable:**
```bash
# Option 1: ENV
export ENV=staging
# Option 2: ENVIRONMENT
export ENVIRONMENT=staging
# Start application
uvicorn main:app --host 0.0.0.0 --port 8000
```
**Or in Docker:**
```dockerfile
ENV ENV=staging
```
**Settings:**
- `should_use_secure_cookies()``True`
- Requires HTTPS
- Moderate logging
### Production Environment
**Set environment variable:**
```bash
# Option 1: ENV
export ENV=production
# Option 2: ENVIRONMENT
export ENVIRONMENT=production
# Start application
uvicorn main:app --host 0.0.0.0 --port 8000
```
**Or in systemd service:**
```ini
[Service]
Environment="ENV=production"
ExecStart=/usr/bin/uvicorn main:app --host 0.0.0.0 --port 8000
```
**Or in Docker Compose:**
```yaml
services:
web:
image: wizamart:latest
environment:
- ENV=production
ports:
- "8000:8000"
```
**Settings:**
- `should_use_secure_cookies()``True`
- Requires HTTPS
- Minimal logging (warnings/errors only)
- Generic error messages
---
## Development Guidelines
### When Writing New Code
#### Use Environment Functions
**DO:**
```python
from app.core.environment import should_use_secure_cookies, is_development
# Use the utility functions
if is_development():
enable_debug_toolbar()
response.set_cookie(secure=should_use_secure_cookies())
```
**DON'T:**
```python
# Don't manually check environment strings
if os.getenv("ENV") == "production": # ❌ Bad
...
# Don't hardcode environment logic
response.set_cookie(secure=True) # ❌ Won't work in development
```
#### Keep Environment Logic Centralized
If you need custom environment-based behavior:
**Option 1: Add to environment.py**
```python
# app/core/environment.py
def should_enable_caching() -> bool:
"""Determine if caching should be enabled."""
return not is_development()
def get_max_upload_size() -> int:
"""Get maximum upload size based on environment."""
env = get_environment()
if env == "development":
return 10 * 1024 * 1024 # 10MB in dev
elif env == "staging":
return 50 * 1024 * 1024 # 50MB in staging
else:
return 100 * 1024 * 1024 # 100MB in prod
```
**Option 2: Use in your module**
```python
from app.core.environment import is_production
def get_cache_ttl() -> int:
"""Get cache TTL based on environment."""
return 3600 if is_production() else 60 # 1 hour in prod, 1 min in dev
```
### Testing Environment Detection
#### Unit Tests
```python
import os
import pytest
from app.core.environment import get_environment, should_use_secure_cookies
def test_environment_detection_with_env_var():
"""Test environment detection with ENV variable."""
os.environ["ENV"] = "production"
# Clear cache first
import app.core.environment as env_module
env_module._cached_environment = None
assert get_environment() == "production"
assert should_use_secure_cookies() == True
# Cleanup
del os.environ["ENV"]
def test_environment_defaults_to_development():
"""Test that environment defaults to development."""
# Ensure no env vars set
os.environ.pop("ENV", None)
os.environ.pop("ENVIRONMENT", None)
# Clear cache
import app.core.environment as env_module
env_module._cached_environment = None
assert get_environment() == "development"
assert should_use_secure_cookies() == False
```
#### Integration Tests
```python
def test_login_sets_secure_cookie_in_production(test_client, monkeypatch):
"""Test that login sets secure cookie in production."""
# Mock production environment
monkeypatch.setenv("ENV", "production")
# Clear cache
import app.core.environment as env_module
env_module._cached_environment = None
# Test login
response = test_client.post("/api/v1/admin/auth/login", json={
"username": "admin",
"password": "admin123"
})
# Check cookie has secure flag
set_cookie_header = response.headers.get("set-cookie")
assert "Secure" in set_cookie_header
```
### Debugging Environment Detection
#### Print Current Environment
```python
# Add to your startup or debug endpoint
from app.core.environment import get_environment, should_use_secure_cookies
@app.on_event("startup")
async def startup_event():
env = get_environment()
secure = should_use_secure_cookies()
print(f"🌍 Environment: {env}")
print(f"🔒 Secure cookies: {secure}")
print(f"📍 Running on: {os.getenv('HOSTNAME', 'localhost')}")
```
#### Debug Endpoint (Development Only)
```python
from app.core.environment import get_environment, is_development
import os
@router.get("/debug/environment")
def debug_environment():
"""Debug endpoint to check environment detection."""
if not is_development():
raise HTTPException(status_code=404)
return {
"detected_environment": get_environment(),
"should_use_secure_cookies": should_use_secure_cookies(),
"env_var_ENV": os.getenv("ENV"),
"env_var_ENVIRONMENT": os.getenv("ENVIRONMENT"),
"env_var_DEBUG": os.getenv("DEBUG"),
"hostname": os.getenv("HOSTNAME", "unknown"),
}
```
---
## Deployment Guide
### Local Development
**Setup:**
```bash
# Clone repo
git clone <wizamart-repo>
cd wizamart-repo
# Create virtual environment
python -m venv venv
source venv/bin/activate # or `venv\Scripts\activate` on Windows
# Install dependencies
pip install -r requirements.txt
# Run application (no environment config needed!)
uvicorn main:app --reload
```
**Access:**
- Admin: http://localhost:8000/admin
- Vendor: http://localhost:8000/vendor/{code}
- Shop: http://localhost:8000/shop
**Environment:**
- Auto-detected as "development"
- Cookies work with HTTP
- Debug mode enabled
### Staging Deployment
**Docker Compose:**
```yaml
version: '3.8'
services:
web:
build: .
environment:
- ENV=staging
- DATABASE_URL=postgresql://user:pass@db:5432/wizamart_staging
ports:
- "8000:8000"
depends_on:
- db
db:
image: postgres:15
environment:
- POSTGRES_DB=wizamart_staging
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
```
**Kubernetes:**
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: wizamart-staging
spec:
template:
spec:
containers:
- name: web
image: wizamart:latest
env:
- name: ENV
value: "staging"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
```
**Requirements:**
- HTTPS enabled (nginx/traefik)
- SSL certificates configured
- Environment variable set: `ENV=staging`
### Production Deployment
**systemd Service:**
```ini
[Unit]
Description=Wizamart Platform
After=network.target
[Service]
User=wizamart
WorkingDirectory=/opt/wizamart
Environment="ENV=production"
Environment="DATABASE_URL=postgresql://user:pass@localhost/wizamart_prod"
ExecStart=/opt/wizamart/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
Restart=always
[Install]
WantedBy=multi-user.target
```
**Docker:**
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV ENV=production
ENV PYTHONUNBUFFERED=1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
```
**Requirements:**
- ✅ HTTPS enabled and enforced
- ✅ SSL certificates valid
- ✅ Environment variable: `ENV=production`
- ✅ Database backups configured
- ✅ Monitoring and logging enabled
- ✅ Security headers configured
### Environment Variable Checklist
| Environment | ENV Variable | HTTPS Required | Debug Mode | Cookie Secure |
|-------------|-------------|----------------|------------|---------------|
| Development | Not set (or "development") | ❌ No | ✅ Yes | ❌ No |
| Staging | "staging" | ✅ Yes | ⚠️ Limited | ✅ Yes |
| Production | "production" | ✅ Yes | ❌ No | ✅ Yes |
---
## Testing
### Manual Testing
#### Test Environment Detection
```bash
# Test development (default)
python -c "
from app.core.environment import get_environment, should_use_secure_cookies
print(f'Environment: {get_environment()}')
print(f'Secure cookies: {should_use_secure_cookies()}')
"
# Expected output:
# Environment: development
# Secure cookies: False
```
```bash
# Test production
ENV=production python -c "
from app.core.environment import get_environment, should_use_secure_cookies
print(f'Environment: {get_environment()}')
print(f'Secure cookies: {should_use_secure_cookies()}')
"
# Expected output:
# Environment: production
# Secure cookies: True
```
#### Test Cookie Behavior
**Development:**
```bash
# Start server (development mode)
uvicorn main:app --reload
# Login and check cookie
curl -X POST http://localhost:8000/api/v1/admin/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}' \
-v 2>&1 | grep -i "set-cookie"
# Should see: Set-Cookie: admin_token=...; HttpOnly; SameSite=Lax; Path=/admin
# Should NOT see: Secure flag
```
**Production:**
```bash
# Start server (production mode)
ENV=production uvicorn main:app
# Login and check cookie
curl -X POST https://yourdomain.com/api/v1/admin/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}' \
-v 2>&1 | grep -i "set-cookie"
# Should see: Set-Cookie: admin_token=...; Secure; HttpOnly; SameSite=Lax; Path=/admin
# Should see: Secure flag present
```
### Automated Tests
#### pytest Examples
```python
# tests/test_environment.py
import pytest
import os
from app.core.environment import (
get_environment,
is_development,
is_staging,
is_production,
should_use_secure_cookies
)
@pytest.fixture(autouse=True)
def clear_environment_cache():
"""Clear cached environment before each test."""
import app.core.environment as env_module
env_module._cached_environment = None
yield
env_module._cached_environment = None
class TestEnvironmentDetection:
def test_default_is_development(self):
"""Test that default environment is development."""
# Clear any env vars
os.environ.pop("ENV", None)
os.environ.pop("ENVIRONMENT", None)
assert get_environment() == "development"
assert is_development() == True
assert is_production() == False
assert should_use_secure_cookies() == False
def test_env_variable_production(self, monkeypatch):
"""Test ENV=production detection."""
monkeypatch.setenv("ENV", "production")
assert get_environment() == "production"
assert is_production() == True
assert should_use_secure_cookies() == True
def test_environment_variable_staging(self, monkeypatch):
"""Test ENVIRONMENT=staging detection."""
monkeypatch.setenv("ENVIRONMENT", "staging")
assert get_environment() == "staging"
assert is_staging() == True
assert should_use_secure_cookies() == True
def test_env_takes_priority_over_environment(self, monkeypatch):
"""Test that ENV variable has priority."""
monkeypatch.setenv("ENV", "production")
monkeypatch.setenv("ENVIRONMENT", "staging")
# ENV should win
assert get_environment() == "production"
def test_debug_true_is_development(self, monkeypatch):
"""Test that DEBUG=true results in development."""
monkeypatch.delenv("ENV", raising=False)
monkeypatch.delenv("ENVIRONMENT", raising=False)
monkeypatch.setenv("DEBUG", "true")
assert get_environment() == "development"
class TestCookieSecurity:
def test_secure_cookies_in_production(self, monkeypatch):
"""Test cookies are secure in production."""
monkeypatch.setenv("ENV", "production")
assert should_use_secure_cookies() == True
def test_insecure_cookies_in_development(self):
"""Test cookies are not secure in development."""
os.environ.pop("ENV", None)
assert should_use_secure_cookies() == False
# tests/test_authentication_cookies.py
def test_login_cookie_matches_environment(test_client, monkeypatch):
"""Test that login cookie security matches environment."""
# Test development
monkeypatch.delenv("ENV", raising=False)
response = test_client.post("/api/v1/admin/auth/login", json={
"username": "admin",
"password": "admin123"
})
set_cookie = response.headers.get("set-cookie", "")
assert "Secure" not in set_cookie # No Secure in development
# Test production
monkeypatch.setenv("ENV", "production")
# Clear cache
import app.core.environment as env_module
env_module._cached_environment = None
response = test_client.post("/api/v1/admin/auth/login", json={
"username": "admin",
"password": "admin123"
})
set_cookie = response.headers.get("set-cookie", "")
assert "Secure" in set_cookie # Secure in production
```
---
## Best Practices
### DO ✅
1. **Use the environment utility functions:**
```python
from app.core.environment import should_use_secure_cookies
response.set_cookie(secure=should_use_secure_cookies())
```
2. **Set ENV in production/staging:**
```bash
export ENV=production
```
3. **Use cached version for frequent calls:**
```python
from app.core.environment import get_cached_environment
env = get_cached_environment() # Cached - faster
```
4. **Keep environment logic in environment.py:**
- Add new environment-dependent functions to `environment.py`
- Don't scatter environment checks throughout codebase
5. **Document environment-dependent behavior:**
```python
def send_email(to: str, subject: str):
"""
Send email to recipient.
Environment behavior:
- Development: Logs email to console (no actual send)
- Staging: Sends to test email address
- Production: Sends to actual recipient
"""
if is_development():
logger.info(f"[DEV] Email to {to}: {subject}")
elif is_staging():
actual_to = f"staging+{to}@example.com"
send_via_smtp(actual_to, subject)
else:
send_via_smtp(to, subject)
```
6. **Test with different environments:**
```python
@pytest.mark.parametrize("env", ["development", "staging", "production"])
def test_feature_in_all_environments(env, monkeypatch):
monkeypatch.setenv("ENV", env)
# Clear cache
import app.core.environment as env_module
env_module._cached_environment = None
# Test your feature
```
### DON'T ❌
1. **Don't hardcode environment checks:**
```python
# ❌ Bad
if os.getenv("ENV") == "production":
use_https = True
# ✅ Good
use_https = should_use_secure_cookies()
```
2. **Don't assume environment:**
```python
# ❌ Bad - assumes production
response.set_cookie(secure=True)
# ✅ Good - adapts to environment
response.set_cookie(secure=should_use_secure_cookies())
```
3. **Don't bypass environment detection:**
```python
# ❌ Bad
FORCE_PRODUCTION = True
if FORCE_PRODUCTION:
...
# ✅ Good - use environment variable
# export ENV=production
if is_production():
...
```
4. **Don't mix environment checking methods:**
```python
# ❌ Bad - inconsistent
if os.getenv("ENV") == "prod":
log_level = "WARNING"
elif is_development():
log_level = "DEBUG"
# ✅ Good - consistent
env = get_environment()
if env == "production":
log_level = "WARNING"
elif env == "development":
log_level = "DEBUG"
```
5. **Don't forget to clear cache in tests:**
```python
# ❌ Bad - cache may interfere
def test_production():
os.environ["ENV"] = "production"
assert get_environment() == "production" # May fail if cached
# ✅ Good - clear cache
def test_production():
import app.core.environment as env_module
env_module._cached_environment = None
os.environ["ENV"] = "production"
assert get_environment() == "production"
```
### Security Considerations
1. **Always use HTTPS in production:**
- Environment detection enables `secure=True` for cookies
- But you must configure HTTPS at the server level
2. **Never disable security in production:**
```python
# ❌ NEVER do this
if is_production():
response.set_cookie(secure=False) # Security vulnerability!
```
3. **Validate environment variable values:**
- The system only accepts: "development", "staging", "production"
- Invalid values default to "development" (safe)
4. **Monitor environment in production:**
```python
@app.on_event("startup")
async def check_production_settings():
if is_production():
# Verify production requirements
assert os.getenv("DATABASE_URL"), "DATABASE_URL required in production"
assert os.getenv("SECRET_KEY"), "SECRET_KEY required in production"
logger.info("✅ Production environment verified")
```
---
## Troubleshooting
### Issue: Cookies Not Working
**Symptom:** Authentication cookies not being sent/received
**Check:**
1. **Environment detection:**
```python
from app.core.environment import get_environment, should_use_secure_cookies
print(f"Environment: {get_environment()}")
print(f"Secure cookies: {should_use_secure_cookies()}")
```
2. **Development (HTTP):**
- `should_use_secure_cookies()` should return `False`
- Access via `http://localhost:8000` (not HTTPS)
3. **Production (HTTPS):**
- `should_use_secure_cookies()` should return `True`
- Access via `https://yourdomain.com` (with HTTPS)
- Verify SSL certificate is valid
### Issue: Wrong Environment Detected
**Symptom:** System detects wrong environment
**Solution:**
```bash
# Explicitly set environment
export ENV=production
# Restart application
uvicorn main:app --reload
# Verify
python -c "from app.core.environment import get_environment; print(get_environment())"
```
### Issue: Environment Not Changing
**Symptom:** Environment stuck on old value
**Cause:** Environment is cached
**Solution:**
```python
# Clear cache and re-detect
import app.core.environment as env_module
env_module._cached_environment = None
# Now check again
from app.core.environment import get_environment
print(get_environment())
```
### Issue: Tests Failing Due to Environment
**Symptom:** Tests pass locally but fail in CI/CD
**Solution:** Always clear cache in test fixtures:
```python
@pytest.fixture(autouse=True)
def clear_environment_cache():
"""Clear environment cache before each test."""
import app.core.environment as env_module
env_module._cached_environment = None
yield
env_module._cached_environment = None
```
---
## Summary
### Quick Reference
```python
# Import
from app.core.environment import (
get_environment,
is_development,
is_production,
should_use_secure_cookies
)
# Usage
env = get_environment() # "development" | "staging" | "production"
if is_development():
# Development-only code
pass
if is_production():
# Production-only code
pass
# Cookie security (most common use case)
response.set_cookie(secure=should_use_secure_cookies())
```
### Configuration
| Environment | Set Variable | Cookie Secure | HTTPS Required |
|-------------|--------------|---------------|----------------|
| Development | Not needed (default) | False | No |
| Staging | `ENV=staging` | True | Yes |
| Production | `ENV=production` | True | Yes |
### Key Points
1. ✅ **Development works out of the box** - No configuration needed
2. ✅ **Production requires explicit setting** - Set `ENV=production`
3.**Use utility functions** - Don't check environment directly
4.**HTTPS required in production** - Configure at server level
5.**Test with different environments** - Use monkeypatch in tests
---
## Additional Resources
### Related Documentation
- [Authentication System Documentation](../api/authentication.md)
- [Deployment Guide](../deployment/production.md)
- [Exception Handling](exception-handling.md)
### External References
- [FastAPI Security](https://fastapi.tiangolo.com/tutorial/security/)
- [HTTP Cookies (MDN)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)
- [12-Factor App: Config](https://12factor.net/config)
---
**Questions?** Contact the backend team or check the main documentation.
**End of Document**