Files
orion/docs/development/environment-detection.md
Samir Boulahtit e9253fbd84 refactor: rename Wizamart to Orion across entire codebase
Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart
with Orion/orion/ORION across 184 files. This includes database
identifiers, email addresses, domain references, R2 bucket names,
DNS prefixes, encryption salt, Celery app name, config defaults,
Docker configs, CI configs, documentation, seed data, and templates.

Renames homepage-wizamart.html template to homepage-orion.html.
Fixes duplicate file_pattern key in api.yaml architecture rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:46:56 +01:00

27 KiB

Environment Detection System

Version: 1.0 Last Updated: November 2025 Audience: Development Team


Table of Contents

  1. Overview
  2. How It Works
  3. Using Environment Detection
  4. Configuration
  5. Development Guidelines
  6. Deployment Guide
  7. Testing
  8. Best Practices

Overview

The Orion 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

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=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

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

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:

  • Developmentsecure=False (works with HTTP)
  • Productionsecure=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: orion: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 <orion-repo>
cd orion-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:

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/orion_staging
    ports:
      - "8000:8000"
    depends_on:
      - db

  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=orion_staging
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass

Kubernetes:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: orion-staging
spec:
  template:
    spec:
      containers:
      - name: web
        image: orion: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=Orion Platform
After=network.target

[Service]
User=orion
WorkingDirectory=/opt/orion
Environment="ENV=production"
Environment="DATABASE_URL=postgresql://user:pass@localhost/orion_prod"
ExecStart=/opt/orion/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

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

  1. Use the environment utility functions:

    from app.core.environment import should_use_secure_cookies
    response.set_cookie(secure=should_use_secure_cookies())
    
  2. Set ENV in production/staging:

    export ENV=production
    
  3. Use cached version for frequent calls:

    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:

    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:

    @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:

    # ❌ Bad
    if os.getenv("ENV") == "production":
        use_https = True
    
    # ✅ Good
    use_https = should_use_secure_cookies()
    
  2. Don't assume environment:

    # ❌ 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:

    # ❌ Bad
    FORCE_PRODUCTION = True
    if FORCE_PRODUCTION:
        ...
    
    # ✅ Good - use environment variable
    # export ENV=production
    if is_production():
        ...
    
  4. 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"
    
  5. 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

  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:

    # ❌ 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:

    @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:

    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:

# 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

  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

External References


Questions? Contact the backend team or check the main documentation.

End of Document