# Middleware Stack The middleware stack is the backbone of the multi-tenant system, handling tenant detection, context injection, and theme loading for all requests. ## Overview The application uses a custom middleware stack that processes **every request** regardless of whether it's: - REST API calls (`/api/*`) - Admin interface pages (`/admin/*`) - Store dashboard pages (`/store/*`) - Storefront pages (`/storefront/*` or custom domains) This middleware layer is **system-wide** and enables the multi-tenant architecture to function seamlessly. ## Middleware Components ### 1. Platform Context Middleware **Purpose**: Detect which platform (OMS, Loyalty, Main) the request is for **What it does**: - Detects platform from: - Custom domain (e.g., `omsflow.lu`, `rewardflow.lu`) - Path prefix in development (e.g., `/platforms/oms/`, `/platforms/loyalty/`) - Default to `main` platform for localhost without prefix - Rewrites path for platform-prefixed requests (strips `/platforms/{code}/`) - Queries database to find platform by domain or code - Injects platform object into `request.state.platform` **URL Detection Logic**: ``` Request arrives │ ▼ ┌─────────────────────────────────────┐ │ Production domain? (omsflow.lu, etc.) │ └─────────────────────────────────────┘ │ YES → Use that platform │ ▼ NO (localhost) ┌─────────────────────────────────────┐ │ Path starts with /platforms/{code}? │ └─────────────────────────────────────┘ │ YES → Strip prefix, use platform │ /platforms/oms/pricing → /pricing │ ▼ NO ┌─────────────────────────────────────┐ │ Use 'main' platform (DEFAULT) │ │ Path unchanged │ └─────────────────────────────────────┘ ``` **Example**: ``` Request: https://localhost:9999/platforms/oms/pricing ↓ Middleware detects: platform_code = "oms" ↓ Rewrites path: /platforms/oms/pricing → /pricing ↓ Queries database: SELECT * FROM platforms WHERE code = 'oms' ↓ Injects: request.state.platform = ``` **Why it's critical**: Without this, the system wouldn't know which platform's content to serve **Configuration**: Runs BEFORE StoreContextMiddleware (sets platform context first) ### 2. Logging Middleware **Purpose**: Request/response logging and performance monitoring **What it does**: - Logs every incoming request with method, path, and client IP - Measures request processing time - Logs response status codes - Adds `X-Process-Time` header with processing duration - Logs errors with stack traces **Example Log Output**: ``` INFO Request: GET /admin/dashboard from 192.168.1.100 INFO Response: 200 for GET /admin/dashboard (0.143s) ``` **Configuration**: Runs first to capture full request timing ### 2. Store Context Middleware **Purpose**: Detect which store's storefront the request is for (multi-tenant core) **What it does**: - Detects store from: - Custom domain (e.g., `customdomain.com`) — via `StoreDomain` lookup (optionally scoped to a platform via `StoreDomain.platform_id`) - Subdomain with two-step lookup: 1. `StorePlatform.custom_subdomain` — per-platform subdomain overrides (e.g., `wizatech-rewards.rewardflow.lu`) 2. `Store.subdomain` — standard subdomain fallback (e.g., `wizatech.omsflow.lu`) - Path prefix (e.g., `/store/store1/` or `/stores/store1/`) - Queries database to find store by domain or code - Injects store object and platform into `request.state.store` - Extracts "clean path" (path without store prefix) - Sets `request.state.clean_path` for routing **Reserved paths**: When a storefront domain is detected, certain paths are excluded from the `/storefront/` rewrite: `/store/`, `/admin/`, `/api/`, `/static/`, `/storefront/`, `/health`, `/docs`, `/redoc`, `/media/`, `/assets/`, and `/merchants/`. This ensures that staff dashboards, APIs, and merchant-level routes are not incorrectly routed to storefront pages. **Example**: ``` Request: https://orion.platform.com/storefront/products ↓ Middleware detects: store_code = "orion" ↓ Queries database: SELECT * FROM stores WHERE code = 'orion' ↓ Injects: request.state.store = request.state.store_id = 1 request.state.clean_path = "/storefront/products" ``` **Why it's critical**: Without this, the system wouldn't know which store's data to show **See**: [Multi-Tenant System](multi-tenant.md) for routing modes **Note on Path-Based Routing:** Previous implementations used a `PathRewriteMiddleware` to rewrite paths at runtime. This has been replaced with **double router mounting** in `main.py`, where storefront routes are registered twice with different prefixes (`/storefront` and `/stores/{store_code}/storefront`). This approach is simpler and uses FastAPI's native routing capabilities. ### 3. Frontend Type Detection Middleware **Purpose**: Determine which frontend the request targets **What it does**: - Uses centralized `FrontendDetector` class for all detection logic - Determines which frontend is being accessed: - `ADMIN` - `/admin/*`, `/api/v1/admin/*` paths or `admin.*` subdomain - `STORE` - `/store/*`, `/api/v1/store/*` paths (management area) - `STOREFRONT` - Customer shop pages (`/storefront/*`, `/stores/*`, store subdomains) - `PLATFORM` - Marketing pages (`/`, `/pricing`, `/about`) - Injects `request.state.frontend_type` (FrontendType enum) **Detection Priority** (handled by `FrontendDetector`): ```python 1. Admin subdomain (admin.omsflow.lu) → ADMIN 2. Path-based detection: - /admin/* or /api/v1/admin/* → ADMIN - /store/* or /api/v1/store/* → STORE - /storefront/*, /stores/* → STOREFRONT - /api/v1/platform/* → PLATFORM 3. Store subdomain (orion.omsflow.lu) → STOREFRONT 4. Store context set by middleware → STOREFRONT 5. Default → PLATFORM ``` **Why it's useful**: Error handlers, templates, and language detection adapt based on frontend type **See**: [Frontend Detection Architecture](frontend-detection.md) for complete details ### 4. Theme Context Middleware **Purpose**: Load store-specific theme settings **What it does**: - Checks if request has a store (from StoreContextMiddleware) - Queries database for store's theme settings - Injects theme configuration into `request.state.theme` - Provides default theme if store has no custom theme **Theme Data Structure**: ```python { "primary_color": "#3B82F6", "secondary_color": "#10B981", "logo_url": "/static/stores/orion/logo.png", "favicon_url": "/static/stores/orion/favicon.ico", "custom_css": "/* store-specific styles */" } ``` **Why it's needed**: Each store storefront can have custom branding ## Naming Conventions ### Middleware File Organization All middleware components follow a consistent naming pattern for maintainability and clarity. #### File Naming: Simple Nouns Without Redundant Suffixes **Pattern**: `{purpose}.py` (no "_middleware" suffix) ``` ✅ Good: middleware/logging.py middleware/store_context.py middleware/auth.py ❌ Avoid: middleware/logging_middleware.py middleware/store_context_middleware.py middleware/auth_middleware.py ``` **Rationale**: - Keeps names concise and consistent - Follows Django, Flask, and FastAPI conventions - Makes imports cleaner: `from middleware.store_context import StoreContextMiddleware` - Reduces redundancy (the `middleware/` directory already indicates the purpose) #### Test File Naming: Mirror the Source File Test files directly mirror the middleware filename with a `test_` prefix: ``` middleware/logging.py → tests/unit/middleware/test_logging.py middleware/store_context.py → tests/unit/middleware/test_store_context.py middleware/auth.py → tests/unit/middleware/test_auth.py ``` #### One Component Per File Each middleware file contains one primary class or a tightly related set of classes: ```python # middleware/logging.py class LoggingMiddleware(BaseHTTPMiddleware): """Request/response logging middleware""" # middleware/frontend_type.py class FrontendTypeMiddleware: # ASGI wrapper for frontend detection # Uses FrontendDetector from app/core/frontend_detector.py # middleware/auth.py class AuthManager: # Authentication logic ``` #### One Test File Per Component Follow the Single Responsibility Principle - each test file tests exactly one component: ``` ✅ Good: tests/unit/middleware/test_logging.py # Tests only LoggingMiddleware tests/unit/middleware/test_store_context.py # Tests only StoreContextManager/Middleware tests/unit/middleware/test_decorators.py # Tests only rate_limit decorator ❌ Avoid: tests/unit/middleware/test_all_middleware.py # Tests multiple components tests/unit/middleware/test_combined.py # Violates SRP ``` **Benefits**: - Easy to locate tests for specific components - Clear test organization and maintainability - Follows unit testing best practices - Simplifies test debugging and updates #### Import Convention When importing middleware components, use explicit imports: ```python # ✅ Preferred - Explicit and clear from middleware.logging import LoggingMiddleware from middleware.store_context import StoreContextManager from middleware.auth import AuthManager # ❌ Avoid - Less clear from middleware import logging_middleware from middleware import store_context_middleware ``` **See**: [Complete Naming Conventions Guide](../development/naming-conventions.md) for project-wide standards. ## Middleware Execution Order ### The Stack (First to Last) ```mermaid graph TD A[Client Request] --> B[1. LoggingMiddleware] B --> C[2. PlatformContextMiddleware] C --> D[3. StoreContextMiddleware] D --> E[4. ContextDetectionMiddleware] E --> F[5. ThemeContextMiddleware] F --> G[6. FastAPI Router] G --> H[Route Handler] H --> I[Response] I --> J[Client] ``` ### Why This Order Matters **Critical Dependencies**: 1. **LoggingMiddleware first** - Needs to wrap everything to measure total time - Must log errors from all other middleware 2. **PlatformContextMiddleware second** - Must run before StoreContextMiddleware (sets platform context) - Rewrites path for `/platforms/{code}/` prefixed requests - Sets `request.state.platform` for downstream middleware 3. **StoreContextMiddleware third** - Uses rewritten path from PlatformContextMiddleware - Must run before ContextDetectionMiddleware (provides store and clean_path) - Must run before ThemeContextMiddleware (provides store_id) 4. **ContextDetectionMiddleware fourth** - Uses clean_path from StoreContextMiddleware - Provides context_type for ThemeContextMiddleware 5. **ThemeContextMiddleware last** - Depends on store from StoreContextMiddleware - Depends on context_type from ContextDetectionMiddleware **Breaking this order will break the application!** **Note:** Path-based routing (e.g., `/stores/{code}/storefront/*`) is handled by double router mounting in `main.py`, not by middleware. Platform path-based routing (e.g., `/platforms/oms/`) IS handled by PlatformContextMiddleware which rewrites the path. ## Request State Variables Middleware components inject these variables into `request.state`: | Variable | Set By | Type | Used By | Description | |----------|--------|------|---------|-------------| | `platform` | PlatformContextMiddleware | Platform | Routes, Content | Current platform object (main, oms, loyalty) | | `platform_context` | PlatformContextMiddleware | dict | Routes | Platform detection details (method, paths) | | `store` | StoreContextMiddleware | Store | Theme, Templates | Current store object | | `store_id` | StoreContextMiddleware | int | Queries, Theme | Current store ID | | `clean_path` | StoreContextMiddleware | str | Context | Path without store prefix (for context detection) | | `context_type` | ContextDetectionMiddleware | RequestContext | Theme, Error handlers | Request context enum | | `theme` | ThemeContextMiddleware | dict | Templates | Store theme config | ### Using in Route Handlers ```python from fastapi import Request @app.get("/storefront/products") async def get_products(request: Request): # Access store store = request.state.store store_id = request.state.store_id # Access context context = request.state.context_type # Access theme theme = request.state.theme # Use in queries products = db.query(Product).filter( Product.store_id == store_id ).all() return {"store": store.name, "products": products} ``` ### Using in Templates ```jinja2 {# Access store #}

{{ request.state.store.name }}

{# Access theme #} {# Access frontend type #} {% if request.state.frontend_type.value == "admin" %}
Admin Mode
{% endif %} ``` ## Request Flow Example ### Example: Storefront Product Page Request **URL**: `https://orion.myplatform.com/storefront/products` **Middleware Processing**: ``` 1. LoggingMiddleware ↓ Starts timer ↓ Logs: "Request: GET /storefront/products from 192.168.1.100" 2. StoreContextMiddleware ↓ Detects subdomain: "orion" ↓ Queries DB: store = get_store_by_code("orion") ↓ Sets: request.state.store = ↓ Sets: request.state.store_id = 1 ↓ Sets: request.state.clean_path = "/storefront/products" 3. FrontendTypeMiddleware ↓ Uses FrontendDetector with path: "/storefront/products" ↓ Has store context: Yes ↓ Detects storefront frontend ↓ Sets: request.state.frontend_type = FrontendType.STOREFRONT 4. ThemeContextMiddleware ↓ Loads theme for store_id = 1 ↓ Sets: request.state.theme = {...theme config...} 5. FastAPI Router ↓ Matches route: @app.get("/storefront/products") ↓ Calls handler function 6. Route Handler ↓ Accesses: request.state.store_id ↓ Queries: products WHERE store_id = 1 ↓ Renders template with store data 8. Response ↓ Returns HTML with store theme 9. LoggingMiddleware (response phase) ↓ Logs: "Response: 200 for GET /storefront/products (0.143s)" ↓ Adds header: X-Process-Time: 0.143 ``` ## Error Handling in Middleware Each middleware component handles errors gracefully: ### StoreContextMiddleware - If store not found: Sets `request.state.store = None` - If database error: Logs error, allows request to continue - Fallback: Request proceeds without store context ### FrontendTypeMiddleware - If clean_path missing: Uses original path - If store missing: Defaults to PLATFORM frontend type - Always sets a frontend_type (never None) ### ThemeContextMiddleware - If store missing: Skips theme loading - If theme query fails: Uses default theme - If no theme exists: Returns empty theme dict **Design Philosophy**: Middleware should never crash the application. Degrade gracefully. ## Performance Considerations ### Database Queries **Per Request**: - 1 query in StoreContextMiddleware (store lookup) - cached by DB - 1 query in ThemeContextMiddleware (theme lookup) - cached by DB **Total**: ~2 DB queries per request **Optimization Opportunities**: - Implement Redis caching for store lookups - Cache theme data in memory - Use connection pooling (already enabled) ### Memory Usage Minimal per-request overhead: - Small objects stored in `request.state` - No global state maintained - Garbage collected after response ### Latency Typical overhead: **< 5ms** per request - Store lookup: ~2ms - Theme lookup: ~2ms - Context detection: <1ms ## Configuration Middleware is registered in `main.py`: ```python # Add in REVERSE order (LIFO execution) app.add_middleware(LoggingMiddleware) app.add_middleware(ThemeContextMiddleware) app.add_middleware(LanguageMiddleware) app.add_middleware(FrontendTypeMiddleware) app.add_middleware(StoreContextMiddleware) app.add_middleware(PlatformContextMiddleware) ``` **Note**: FastAPI's `add_middleware` executes in **reverse order** (Last In, First Out) ## Testing Middleware ### Unit Testing Test each middleware component in isolation: ```python from middleware.store_context import StoreContextManager def test_store_detection_subdomain(): # Mock request request = create_mock_request(host="orion.platform.com") # Test detection manager = StoreContextManager() store = manager.detect_store_from_subdomain(request) assert store.code == "orion" ``` ### Integration Testing Test the full middleware stack: ```python def test_storefront_request_flow(client): response = client.get( "/storefront/products", headers={"Host": "orion.platform.com"} ) assert response.status_code == 200 assert "Orion" in response.text ``` **See**: [Testing Guide](../testing/testing-guide.md) ## Debugging Middleware ### Enable Debug Logging ```python import logging logging.getLogger("middleware").setLevel(logging.DEBUG) ``` ### Check Request State In route handlers: ```python @app.get("/debug") async def debug_state(request: Request): return { "store": request.state.store.name if hasattr(request.state, 'store') else None, "store_id": getattr(request.state, 'store_id', None), "clean_path": getattr(request.state, 'clean_path', None), "context_type": request.state.context_type.value if hasattr(request.state, 'context_type') else None, "theme": bool(getattr(request.state, 'theme', None)) } ``` ### Common Issues | Issue | Cause | Solution | |-------|-------|----------| | Store not detected | Wrong host header | Check domain configuration | | Context is FALLBACK | Path doesn't match patterns | Check route prefix | | Theme not loading | Store ID missing | Check StoreContextMiddleware runs first | | Sidebar broken | Variable name conflict | See frontend troubleshooting | ## Related Documentation - [Multi-Tenant System](multi-tenant.md) - Detailed routing modes - [Request Flow](request-flow.md) - Complete request journey - [Authentication & RBAC](auth-rbac.md) - Security middleware - [Backend API Reference](../backend/middleware-reference.md) - Technical API docs - [Frontend Development](../frontend/overview.md) - Using middleware state in frontend ## Technical Reference For detailed API documentation of middleware classes and methods, see: - [Backend Middleware Reference](../backend/middleware-reference.md) This includes: - Complete class documentation - Method signatures - Parameter details - Return types - Auto-generated from source code