27 KiB
Environment Detection System
Version: 1.0 Last Updated: November 2025 Audience: Development Team
Table of Contents
- Overview
- How It Works
- Using Environment Detection
- Configuration
- Development Guidelines
- Deployment Guide
- Testing
- 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:
- Cookie Security - Should cookies require HTTPS? (
secureflag) - Debug Mode - Enable detailed error messages in development
- CORS Settings - Stricter in production, relaxed in development
- 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
ENV=production
ENV=staging
ENV=development # or "dev" or "local"
Priority 2: ENVIRONMENT Variable
ENVIRONMENT=production
ENVIRONMENT=staging
ENVIRONMENT=development
Priority 3: Auto-Detection
The system checks for indicators:
Development Indicators:
DEBUG=trueenvironment 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
app/core/environment.py
Available Functions
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
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
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
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
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
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
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:
# 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:
ENV ENV=staging
Settings:
should_use_secure_cookies()→True- Requires HTTPS
- Moderate logging
Production Environment
Set environment variable:
# 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:
[Service]
Environment="ENV=production"
ExecStart=/usr/bin/uvicorn main:app --host 0.0.0.0 --port 8000
Or in Docker Compose:
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:
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:
# 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
# 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
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
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
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
# 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)
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:
# 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:
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:
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:
[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:
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
# 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
# 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:
# 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:
# 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
# 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 ✅
-
Use the environment utility functions:
from app.core.environment import should_use_secure_cookies response.set_cookie(secure=should_use_secure_cookies()) -
Set ENV in production/staging:
export ENV=production -
Use cached version for frequent calls:
from app.core.environment import get_cached_environment env = get_cached_environment() # Cached - faster -
Keep environment logic in environment.py:
- Add new environment-dependent functions to
environment.py - Don't scatter environment checks throughout codebase
- Add new environment-dependent functions to
-
Document environment-dependent behavior:
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) -
Test with different environments:
@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 ❌
-
Don't hardcode environment checks:
# ❌ Bad if os.getenv("ENV") == "production": use_https = True # ✅ Good use_https = should_use_secure_cookies() -
Don't assume environment:
# ❌ Bad - assumes production response.set_cookie(secure=True) # ✅ Good - adapts to environment response.set_cookie(secure=should_use_secure_cookies()) -
Don't bypass environment detection:
# ❌ Bad FORCE_PRODUCTION = True if FORCE_PRODUCTION: ... # ✅ Good - use environment variable # export ENV=production if is_production(): ... -
Don't mix environment checking methods:
# ❌ 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" -
Don't forget to clear cache in tests:
# ❌ 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
-
Always use HTTPS in production:
- Environment detection enables
secure=Truefor cookies - But you must configure HTTPS at the server level
- Environment detection enables
-
Never disable security in production:
# ❌ NEVER do this if is_production(): response.set_cookie(secure=False) # Security vulnerability! -
Validate environment variable values:
- The system only accepts: "development", "staging", "production"
- Invalid values default to "development" (safe)
-
Monitor environment in production:
@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:
-
Environment detection:
from app.core.environment import get_environment, should_use_secure_cookies print(f"Environment: {get_environment()}") print(f"Secure cookies: {should_use_secure_cookies()}") -
Development (HTTP):
should_use_secure_cookies()should returnFalse- Access via
http://localhost:8000(not HTTPS)
-
Production (HTTPS):
should_use_secure_cookies()should returnTrue- Access via
https://yourdomain.com(with HTTPS) - Verify SSL certificate is valid
Issue: Wrong Environment Detected
Symptom: System detects wrong environment
Solution:
# 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:
# 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:
@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
# 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
- ✅ Development works out of the box - No configuration needed
- ✅ Production requires explicit setting - Set
ENV=production - ✅ Use utility functions - Don't check environment directly
- ✅ HTTPS required in production - Configure at server level
- ✅ Test with different environments - Use monkeypatch in tests
Additional Resources
Related Documentation
External References
Questions? Contact the backend team or check the main documentation.
End of Document