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>
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).
1. SUBDOMAIN MODE (Production - Recommended)
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
1. SUBDOMAIN MODE (Production - Recommended)
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:
- Customer visits
https://acme.orion.lu/storefront/products store_context_middlewaredetects subdomain"acme"- Queries:
SELECT * FROM stores WHERE subdomain = 'acme' - Finds Store with ID=1 (ACME Store)
- Sets
request.state.store = Store(ACME Store) context_middlewaredetects it's a STOREFRONT requesttheme_context_middlewareloads ACME's theme- Routes to
storefront_pages.py→storefront_products_page() - 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:
- Customer visits
https://store.acme-corp.com/storefront/products store_context_middlewaredetects custom domain (not *.orion.lu, not localhost)- Normalizes domain to
"store.acme-corp.com" - Queries:
SELECT * FROM store_domains WHERE domain = 'store.acme-corp.com' - Finds
StoreDomainwithstore_id = 1 - Joins to get
Store(ACME Store) - 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:
- Developer visits
http://localhost:8000/platforms/oms/stores/acme/storefront/products - Platform middleware detects
/platforms/oms/prefix, sets platform context store_context_middlewaredetects path-based routing pattern/stores/acme/...- Extracts store code
"acme"from the path - Looks up Store:
SELECT * FROM stores WHERE subdomain = 'acme' - Sets
request.state.store = Store(acme) - 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.storeis 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_idforeign 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:
-
For Subdomain/Custom Domain Mode:
- URL:
https://acme.orion.lu/storefront/products - Matches: First router with
/storefrontprefix - Route:
@router.get("/products")→ Full path:/storefront/products
- URL:
-
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}/storefrontprefix - Route:
@router.get("/products")→ Full path:/stores/{store_code}/storefront/products - Bonus:
store_codeavailable as path parameter!
- URL:
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
- For Production: Use subdomain or custom domain mode
- For Development: Use path-based mode locally
- For Deployment: Configure DNS for subdomains or custom domains
- For Testing: Create test stores with different themes
- For Scaling: Consider CDN for store-specific assets
Generated: January 30, 2026 Orion Version: Current Development