Files
orion/docs/architecture/middleware.md
Samir Boulahtit 319900623a
Some checks failed
CI / ruff (push) Successful in 14s
CI / pytest (push) Failing after 50m12s
CI / validate (push) Successful in 25s
CI / dependency-scanning (push) Successful in 32s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
feat: add SQL query tool, platform debug, loyalty settings, and multi-module improvements
- Add admin SQL query tool with saved queries, schema explorer presets,
  and collapsible category sections (dev_tools module)
- Add platform debug tool for admin diagnostics
- Add loyalty settings page with owner-only access control
- Fix loyalty settings owner check (use currentUser instead of window.__userData)
- Replace HTTPException with AuthorizationException in loyalty routes
- Expand loyalty module with PIN service, Apple Wallet, program management
- Improve store login with platform detection and multi-platform support
- Update billing feature gates and subscription services
- Add store platform sync improvements and remove is_primary column
- Add unit tests for loyalty (PIN, points, stamps, program services)
- Update i18n translations across dev_tools locales

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 20:08:07 +01:00

21 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 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 = <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-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 = <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 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):

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

Login Platform Resolution

The store login endpoint (POST /api/v1/store/auth/login) resolves the platform through a 3-source priority chain. This is necessary because on localhost the API path carries no platform information (unlike production where the domain does).

Source Priority

Source 1: Middleware (request.state.platform)
    ↓ if null or "main"
Source 2: Request body (platform_code field)
    ↓ if null
Source 3: Fallback (store's first active platform)

Resolution by URL Pattern

Environment Login Page URL API Request Host Source 1 Source 2 Source 3
Dev path-based /platforms/loyalty/store/ACME/login localhost:8000 null (localhost → "main" → skipped) "loyalty" from JS
Dev no prefix /store/ACME/login (after logout) localhost:8000 null "loyalty" from localStorage
Dev fresh browser /store/ACME/login (first visit) localhost:8000 null null First active platform for store
Prod domain omsflow.lu/store/ACME/login omsflow.lu "oms" (domain lookup)
Prod subdomain acme.omsflow.lu/store/login acme.omsflow.lu "oms" (root domain lookup)
Prod custom domain wizatech.shop/store/login wizatech.shop "oms" (StoreDomain lookup)

Client-Side Platform Persistence

On successful login, login.js saves the platform to localStorage:

localStorage.setItem('store_platform', response.platform_code)

On the login page, the platform_code sent in the body uses this priority:

window.STORE_PLATFORM_CODE || localStorage.getItem('store_platform') || null
  • window.STORE_PLATFORM_CODE is set by the server template when the URL contains /platforms/{code}/
  • localStorage.store_platform persists across logout (intentionally not cleared)
  • This ensures the logout → login cycle preserves platform context in dev mode

Diagnostic Tools

  • Backend: /admin/platform-debug — traces the full resolution pipeline for arbitrary host/path combos
  • Frontend: Ctrl+Shift+P on any store page (localhost only) — shows JWT platform, localStorage, window globals, and consistency checks

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:

  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

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

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