Reflect the production routing refactor (ce5b54f): document store dashboard
double-mounting, per-platform subdomain overrides via StorePlatform.custom_subdomain,
get_resolved_store_code dependency, and /merchants/ reserved path. Update seed
script to populate custom_subdomain and StoreDomain.platform_id for demo data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
19 KiB
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
mainplatform for localhost without prefix
- Custom domain (e.g.,
- 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 = <Platform object>
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-Timeheader 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) — viaStoreDomainlookup (optionally scoped to a platform viaStoreDomain.platform_id) - Subdomain with two-step lookup:
StorePlatform.custom_subdomain— per-platform subdomain overrides (e.g.,wizatech-rewards.rewardflow.lu)Store.subdomain— standard subdomain fallback (e.g.,wizatech.omsflow.lu)
- Path prefix (e.g.,
/store/store1/or/stores/store1/)
- Custom domain (e.g.,
- 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_pathfor 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 = <Store object>
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 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
FrontendDetectorclass for all detection logic - Determines which frontend is being accessed:
ADMIN-/admin/*,/api/v1/admin/*paths oradmin.*subdomainSTORE-/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):
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 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:
{
"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:
# 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:
# ✅ 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 for project-wide standards.
Middleware Execution Order
The Stack (First to Last)
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:
-
LoggingMiddleware first
- Needs to wrap everything to measure total time
- Must log errors from all other middleware
-
PlatformContextMiddleware second
- Must run before StoreContextMiddleware (sets platform context)
- Rewrites path for
/platforms/{code}/prefixed requests - Sets
request.state.platformfor downstream middleware
-
StoreContextMiddleware third
- Uses rewritten path from PlatformContextMiddleware
- Must run before ContextDetectionMiddleware (provides store and clean_path)
- Must run before ThemeContextMiddleware (provides store_id)
-
ContextDetectionMiddleware fourth
- Uses clean_path from StoreContextMiddleware
- Provides context_type for ThemeContextMiddleware
-
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
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
{# Access store #}
<h1>{{ request.state.store.name }}</h1>
{# Access theme #}
<style>
:root {
--primary-color: {{ request.state.theme.primary_color }};
--secondary-color: {{ request.state.theme.secondary_color }};
}
</style>
{# Access frontend type #}
{% if request.state.frontend_type.value == "admin" %}
<div class="admin-badge">Admin Mode</div>
{% 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 = <Store: Orion>
↓ 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:
# 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:
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:
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
Debugging Middleware
Enable Debug Logging
import logging
logging.getLogger("middleware").setLevel(logging.DEBUG)
Check Request State
In route handlers:
@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 - Detailed routing modes
- Request Flow - Complete request journey
- Authentication & RBAC - Security middleware
- Backend API Reference - Technical API docs
- Frontend Development - Using middleware state in frontend
Technical Reference
For detailed API documentation of middleware classes and methods, see:
This includes:
- Complete class documentation
- Method signatures
- Parameter details
- Return types
- Auto-generated from source code