Files
orion/docs/architecture/url-routing/overview.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

20 KiB

Orion Multi-Tenant URL Routing Guide

Quick Answer

How do customers access a store's storefront in Orion?

There are three ways depending on the deployment mode:

⚠️ Important: This guide describes customer-facing storefront routes. For store dashboard/management routes, see Store Frontend Architecture. The storefront uses /stores/{code}/storefront/* (plural) in path-based mode, while the store dashboard uses /store/{code}/* (singular).

https://STORE_SUBDOMAIN.platform.com/storefront/products

Example:
https://acme.orion.lu/storefront/products
https://techpro.orion.lu/storefront/categories/electronics

2. CUSTOM DOMAIN MODE (Production - Premium)

https://STORE_CUSTOM_DOMAIN/storefront/products

Example:
https://store.acmecorp.com/storefront/products
https://shop.techpro.io/storefront/cart

3. PATH-BASED MODE (Development Only)

http://localhost:PORT/platforms/PLATFORM_CODE/stores/STORE_CODE/storefront/products

Example:
http://localhost:8000/platforms/oms/stores/acme/storefront/products
http://localhost:8000/platforms/loyalty/stores/techpro/storefront/checkout

Multi-Platform URL Routing

Orion supports multiple platforms (OMS, Loyalty, Site Builder), each with its own marketing site and store ecosystem.

Platform URL Structure

Development Mode (localhost)

URL What it serves
/ Main marketing site homepage (main platform)
/about Main marketing site about page
/platforms/oms/ OMS platform homepage
/platforms/oms/pricing OMS platform pricing page
/platforms/oms/stores/{code}/storefront/ Store storefront on OMS
/platforms/oms/admin/ Admin panel for OMS platform
/platforms/oms/store/{code}/ Store dashboard on OMS
/platforms/loyalty/ Loyalty platform homepage
/platforms/loyalty/features Loyalty platform features page

Production Mode (custom domains)

URL What it serves
orion.lu/ Main marketing site homepage
orion.lu/about Main marketing site about page
oms.lu/ OMS platform homepage
oms.lu/pricing OMS platform pricing page
oms.lu/admin/ Admin panel for OMS platform
oms.lu/store/{code}/ Store dashboard on OMS
https://mybakery.lu/storefront/ Store storefront (store's custom domain)
loyalty.lu/ Loyalty platform homepage

Note: In production, stores configure their own custom domains for storefronts. The platform domain (e.g., oms.lu) is used for admin and store dashboards, while storefronts use store-owned domains.

Quick Reference by Platform

For "oms" Platform

Dev:
  Platform:   http://localhost:8000/platforms/oms/
  Admin:      http://localhost:8000/platforms/oms/admin/
  Store:     http://localhost:8000/platforms/oms/store/{store_code}/
  Storefront: http://localhost:8000/platforms/oms/stores/{store_code}/storefront/

Prod:
  Platform:   https://oms.lu/
  Admin:      https://oms.lu/admin/
  Store:     https://oms.lu/store/{store_code}/
  Storefront: https://mybakery.lu/storefront/  (store's custom domain)

For "loyalty" Platform

Dev:
  Platform:   http://localhost:8000/platforms/loyalty/
  Admin:      http://localhost:8000/platforms/loyalty/admin/
  Store:     http://localhost:8000/platforms/loyalty/store/{store_code}/
  Storefront: http://localhost:8000/platforms/loyalty/stores/{store_code}/storefront/

Prod:
  Platform:   https://loyalty.lu/
  Admin:      https://loyalty.lu/admin/
  Store:     https://loyalty.lu/store/{store_code}/
  Storefront: https://myrewards.lu/storefront/  (store's custom domain)

Platform Routing Logic

Request arrives
      │
      ▼
┌─────────────────────────────────────┐
│ Check: Is this production domain?   │
│ (oms.lu, loyalty.lu, etc.)          │
└─────────────────────────────────────┘
      │
      ├── YES → Route to that platform
      │
      ▼ NO (localhost)
┌─────────────────────────────────────┐
│ Check: Does path start with         │
│ /platforms/{code}/ ?                │
└─────────────────────────────────────┘
      │
      ├── YES → Strip prefix, route to platform
      │         /platforms/oms/pricing → /pricing on OMS
      │
      ▼ NO
┌─────────────────────────────────────┐
│ Route to MAIN MARKETING SITE        │
│ (no platform context)               │
│ /faq → Main site FAQ page           │
└─────────────────────────────────────┘

Platform Codes

Platform Code Dev URL Prod Domain
Main Marketing main localhost:8000/ orion.lu
OMS oms localhost:8000/platforms/oms/ oms.lu
Loyalty loyalty localhost:8000/platforms/loyalty/ loyalty.lu
Site Builder site-builder localhost:8000/platforms/site-builder/ sitebuilder.lu

See: Multi-Platform CMS Architecture for content management details.


Three Deployment Modes Explained

URL Pattern: https://STORE_SUBDOMAIN.platform.com/storefront/...

Example:

  • Store subdomain: acme
  • Platform domain: orion.lu
  • Customer Storefront URL: https://acme.orion.lu/storefront/products
  • Product Detail: https://acme.orion.lu/storefront/products/123

How It Works:

  1. Customer visits https://acme.orion.lu/storefront/products
  2. store_context_middleware detects subdomain "acme"
  3. Queries: SELECT * FROM stores WHERE subdomain = 'acme'
  4. Finds Store with ID=1 (ACME Store)
  5. Sets request.state.store = Store(ACME Store)
  6. context_middleware detects it's a STOREFRONT request
  7. theme_context_middleware loads ACME's theme
  8. Routes to storefront_pages.pystorefront_products_page()
  9. Renders template with ACME's colors, logo, and products

Advantages:

  • Single SSL certificate for all stores (*.orion.lu)
  • Easy to manage DNS (just add subdomains)
  • Customers don't need to bring their own domain

2. CUSTOM DOMAIN MODE (Production - Premium)

URL Pattern: https://CUSTOM_DOMAIN/storefront/...

Example:

  • Store name: "ACME Store"
  • Custom domain: store.acme-corp.com
  • Customer Storefront URL: https://store.acme-corp.com/storefront/products

Database Setup:

-- stores table
id | name        | subdomain
1  | ACME Store  | acme

-- store_domains table (links custom domains to stores)
id | store_id | domain              | is_active | is_verified
1  | 1         | store.acme-corp.com | true      | true

How It Works:

  1. Customer visits https://store.acme-corp.com/storefront/products
  2. store_context_middleware detects custom domain (not *.orion.lu, not localhost)
  3. Normalizes domain to "store.acme-corp.com"
  4. Queries: SELECT * FROM store_domains WHERE domain = 'store.acme-corp.com'
  5. Finds StoreDomain with store_id = 1
  6. Joins to get Store(ACME Store)
  7. Rest is same as subdomain mode...

Advantages:

  • Professional branding with store's own domain
  • Better for premium stores
  • Store controls the domain

Considerations:

  • Each store needs their own SSL certificate
  • Store must own and configure the domain

3. PATH-BASED MODE (Development Only)

URL Pattern: http://localhost:PORT/platforms/PLATFORM_CODE/stores/STORE_CODE/storefront/...

Example:

  • Development: http://localhost:8000/platforms/oms/stores/acme/storefront/products
  • With port: http://localhost:8000/platforms/loyalty/stores/acme/storefront/products/123

How It Works:

  1. Developer visits http://localhost:8000/platforms/oms/stores/acme/storefront/products
  2. Platform middleware detects /platforms/oms/ prefix, sets platform context
  3. store_context_middleware detects path-based routing pattern /stores/acme/...
  4. Extracts store code "acme" from the path
  5. Looks up Store: SELECT * FROM stores WHERE subdomain = 'acme'
  6. Sets request.state.store = Store(acme)
  7. Routes to storefront pages

Advantages:

  • Perfect for local development
  • No need to configure DNS/domains
  • Test multiple stores and platforms easily without domain setup

Limitations:

  • Only for development (not production-ready)
  • All stores share same localhost address

Complete Route Examples

Subdomain/Custom Domain (PRODUCTION)

https://acme.orion.lu/storefront/                        → Homepage
https://acme.orion.lu/storefront/products                → Product Catalog
https://acme.orion.lu/storefront/products/123            → Product Detail
https://acme.orion.lu/storefront/categories/electronics  → Category Page
https://acme.orion.lu/storefront/cart                    → Shopping Cart
https://acme.orion.lu/storefront/checkout                → Checkout
https://acme.orion.lu/storefront/search?q=laptop         → Search Results
https://acme.orion.lu/storefront/account/login           → Customer Login
https://acme.orion.lu/storefront/account/dashboard       → Account Dashboard (Auth Required)
https://acme.orion.lu/storefront/account/orders          → Order History (Auth Required)
https://acme.orion.lu/storefront/account/profile         → Profile (Auth Required)

Path-Based (DEVELOPMENT)

http://localhost:8000/platforms/oms/stores/acme/storefront/                → Homepage
http://localhost:8000/platforms/oms/stores/acme/storefront/products        → Products
http://localhost:8000/platforms/oms/stores/acme/storefront/products/123    → Product Detail
http://localhost:8000/platforms/oms/stores/acme/storefront/cart            → Cart
http://localhost:8000/platforms/oms/stores/acme/storefront/checkout        → Checkout
http://localhost:8000/platforms/oms/stores/acme/storefront/account/login   → Login

API Endpoints (Same for All Modes)

GET  /api/v1/storefront/stores/1/products              → Get store products
GET  /api/v1/storefront/stores/1/products/123          → Get product details
POST /api/v1/storefront/stores/1/products/{id}/reviews → Add product review

How Store Isolation Works

Multi-Layer Enforcement

Layer 1: URL Routing

  • Store is detected from subdomain, custom domain, or path
  • Each store gets their own request context

Layer 2: Middleware

  • request.state.store is set to the detected Store object
  • All downstream code can access the store

Layer 3: Database Queries

  • All queries must include WHERE store_id = ?
  • Product queries: SELECT * FROM products WHERE store_id = 1
  • Order queries: SELECT * FROM orders WHERE store_id = 1

Layer 4: API Authorization

  • Endpoints verify the store matches the request store
  • Customers can only see their own store's products

Example: No Cross-Store Leakage

# Customer on acme.orion.lu tries to access TechPro's products
# They make API call to /api/v1/storefront/stores/2/products

# Backend checks:
store = get_store_from_request(request)  # Returns Store(id=1, name="ACME")
if store.id != requested_store_id:       # if 1 != 2
    raise UnauthorizedStorefrontAccessException()

Request Lifecycle: Complete Flow

Scenario: Customer visits https://acme.orion.lu/storefront/products

┌─────────────────────────────────────────────────────────────────┐
│ 1. REQUEST ARRIVES                                              │
└─────────────────────────────────────────────────────────────────┘
   method: GET
   host: acme.orion.lu
   path: /storefront/products

┌─────────────────────────────────────────────────────────────────┐
│ 2. MIDDLEWARE CHAIN                                             │
└─────────────────────────────────────────────────────────────────┘

   A) store_context_middleware
      ├─ Detects host: "acme.orion.lu"
      ├─ Extracts subdomain: "acme"
      ├─ Queries: SELECT * FROM stores WHERE subdomain = 'acme'
      └─ Sets: request.state.store = Store(ACME Store)

   B) context_middleware
      ├─ Checks path: "/storefront/products"
      ├─ Has request.state.store? YES
      └─ Sets: request.state.context_type = RequestContext.STOREFRONT

   C) theme_context_middleware
      ├─ Queries: SELECT * FROM store_themes WHERE store_id = 1
      └─ Sets: request.state.theme = {...ACME's theme...}

┌─────────────────────────────────────────────────────────────────┐
│ 3. ROUTE MATCHING                                               │
└─────────────────────────────────────────────────────────────────┘
   Path: /storefront/products
   Matches: @router.get("/storefront/products")
   Handler: storefront_products_page(request)

┌─────────────────────────────────────────────────────────────────┐
│ 4. HANDLER EXECUTES                                             │
└─────────────────────────────────────────────────────────────────┘
   @router.get("/storefront/products", response_class=HTMLResponse)
   async def storefront_products_page(request: Request):
       return templates.TemplateResponse(
           "storefront/products.html",
           {"request": request}
       )

┌─────────────────────────────────────────────────────────────────┐
│ 5. TEMPLATE RENDERS                                             │
└─────────────────────────────────────────────────────────────────┘
   Template accesses:
   ├─ request.state.store.name → "ACME Store"
   ├─ request.state.theme.colors.primary → "#FF6B6B"
   ├─ request.state.theme.branding.logo → "acme-logo.png"
   └─ Products will load via JavaScript API call

┌─────────────────────────────────────────────────────────────────┐
│ 6. JAVASCRIPT LOADS PRODUCTS (Client-Side)                     │
└─────────────────────────────────────────────────────────────────┘
   fetch(`/api/v1/storefront/stores/1/products`)
   .then(data => renderProducts(data.products, {theme}))

┌─────────────────────────────────────────────────────────────────┐
│ 7. RESPONSE SENT                                                │
└─────────────────────────────────────────────────────────────────┘
   HTML with ACME's colors, logo, and products

Theme Integration

Each store's storefront is fully branded with their custom theme:

# Theme loaded for https://acme.orion.lu
request.state.theme = {
    "theme_name": "modern",
    "colors": {
        "primary": "#FF6B6B",
        "secondary": "#FF8787",
        "accent": "#FF5252",
        "background": "#ffffff",
        "text": "#1f2937"
    },
    "branding": {
        "logo": "acme-logo.png",
        "favicon": "acme-favicon.ico",
        "banner": "acme-banner.jpg"
    },
    "fonts": {
        "heading": "Poppins, sans-serif",
        "body": "Inter, sans-serif"
    }
}

In Jinja2 template:

<style>
  :root {
    --color-primary: {{ request.state.theme.colors.primary }};
    --color-secondary: {{ request.state.theme.colors.secondary }};
  }
</style>

<img src="{{ request.state.theme.branding.logo }}" alt="{{ request.state.store.name }}" />
<h1 style="font-family: {{ request.state.theme.fonts.heading }}">
  Welcome to {{ request.state.store.name }}
</h1>

Key Points for Understanding

1. Customer Perspective

  • Customers just visit a URL (like any normal e-commerce site)
  • They have no awareness it's a multi-tenant platform
  • Each store looks completely separate and branded

2. Store Perspective

  • Stores can use a subdomain (free/standard): acme.orion.lu
  • Or their own custom domain (premium): store.acme-corp.com
  • Both routes go to the exact same backend code

3. Developer Perspective

  • The middleware layer detects which store is being accessed
  • All business logic remains store-unaware
  • Database queries automatically filtered by store
  • No risk of data leakage because of multi-layer isolation

4. Tech Stack

  • Frontend: Jinja2 templates + Alpine.js + Tailwind CSS
  • Backend: FastAPI + SQLAlchemy
  • Auth: JWT with store-scoped cookies
  • Database: All tables have store_id foreign key

Path-Based Routing Implementation

Current Solution: Double Router Mounting

The application handles path-based routing by registering storefront routes twice with different prefixes:

# In main.py
app.include_router(storefront_pages.router, prefix="/storefront")
app.include_router(storefront_pages.router, prefix="/stores/{store_code}/storefront")

How This Works:

  1. For Subdomain/Custom Domain Mode:

    • URL: https://acme.orion.lu/storefront/products
    • Matches: First router with /storefront prefix
    • Route: @router.get("/products") → Full path: /storefront/products
  2. For Path-Based Development Mode:

    • URL: http://localhost:8000/platforms/oms/stores/acme/storefront/products
    • Platform middleware strips /platforms/oms/ prefix, sets platform context
    • Matches: Second router with /stores/{store_code}/storefront prefix
    • Route: @router.get("/products") → Full path: /stores/{store_code}/storefront/products
    • Bonus: store_code available as path parameter!

Benefits:

  • No middleware complexity or path manipulation
  • FastAPI native routing
  • Explicit and maintainable
  • Store code accessible via path parameter when needed
  • Both deployment modes supported cleanly

Authentication in Multi-Tenant Storefront

Customer authentication uses store-scoped cookies:

# Login sets cookie scoped to store's storefront
Set-Cookie: customer_token=eyJ...; Path=/storefront; HttpOnly; SameSite=Lax

# This prevents:
# - Tokens leaking across stores
# - Cross-site request forgery
# - Cookie scope confusion in multi-tenant setup

Summary Table

Mode URL Use Case SSL DNS
Subdomain store.platform.com/storefront Production (standard) *.platform.com Add subdomains
Custom Domain store-domain.com/storefront Production (premium) Per store Store configures
Path-Based localhost:8000/platforms/{p}/stores/{v}/storefront Development only None None

Next Steps

  1. For Production: Use subdomain or custom domain mode
  2. For Development: Use path-based mode locally
  3. For Deployment: Configure DNS for subdomains or custom domains
  4. For Testing: Create test stores with different themes
  5. For Scaling: Consider CDN for store-specific assets

Generated: January 30, 2026 Orion Version: Current Development