# 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](../../frontend/store/architecture.md). The storefront uses `/platforms/{platform_code}/storefront/{store_code}/*` in dev path-based mode, while the store dashboard uses `/platforms/{platform_code}/store/{store_code}/*`. In production, the domain IS the storefront (root path `/`), and staff access is at `/store/`. ### 1. **SUBDOMAIN MODE** (Production - Recommended) ``` https://STORE_SUBDOMAIN.platform-domain.lu/ https://STORE_SUBDOMAIN.platform-domain.lu/products https://STORE_SUBDOMAIN.platform-domain.lu/cart Example: https://acme.omsflow.lu/ https://acme.omsflow.lu/products https://techpro.rewardflow.lu/account/dashboard ``` ### 2. **CUSTOM DOMAIN MODE** (Production - Premium) ``` https://STORE_CUSTOM_DOMAIN/ https://STORE_CUSTOM_DOMAIN/products Example: https://store.acmecorp.com/ https://shop.techpro.io/cart ``` ### 3. **PATH-BASED MODE** (Development Only) ``` http://localhost:PORT/platforms/PLATFORM_CODE/storefront/STORE_CODE/ http://localhost:PORT/platforms/PLATFORM_CODE/storefront/STORE_CODE/products Example: http://localhost:8000/platforms/oms/storefront/ACME/products http://localhost:8000/platforms/loyalty/storefront/TECHPRO/cart ``` --- ## 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/storefront/{code}/` | 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 | | `omsflow.lu/` | OMS platform homepage | | `omsflow.lu/pricing` | OMS platform pricing page | | `omsflow.lu/admin/` | Admin panel for OMS platform | | `omsflow.lu/store/{code}/` | Store dashboard on OMS | | `mybakery.omsflow.lu/` | Store storefront (subdomain) | | `https://mybakery.lu/` | Store storefront (custom domain) | | `rewardflow.lu/` | Loyalty platform homepage | **Note:** In production, storefronts are accessed via subdomain (`store.omsflow.lu`) or custom domain (`mybakery.lu`). The root path `/` IS the storefront — the `PlatformContextMiddleware` internally rewrites it to `/storefront/`. Staff dashboards are at `/store/` on the same domain. ### 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/storefront/{store_code}/ Prod: Platform: https://omsflow.lu/ Admin: https://omsflow.lu/admin/ Store: https://{store}.omsflow.lu/store/ Storefront: https://{store}.omsflow.lu/ (subdomain) Storefront: https://mybakery.lu/ (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/storefront/{store_code}/ Prod: Platform: https://rewardflow.lu/ Admin: https://rewardflow.lu/admin/ Store: https://{store}.rewardflow.lu/store/ Storefront: https://{store}.rewardflow.lu/ (subdomain) Storefront: https://myrewards.lu/ (custom domain) ``` ### Platform Routing Logic ``` Request arrives │ ▼ ┌─────────────────────────────────────┐ │ Check: Is this production domain? │ │ (omsflow.lu, rewardflow.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/` | `omsflow.lu` | | Loyalty | `loyalty` | `localhost:8000/platforms/loyalty/` | `rewardflow.lu` | | Site Builder | `site-builder` | `localhost:8000/platforms/site-builder/` | `sitebuilder.lu` | **See:** [Multi-Platform CMS Architecture](../multi-platform-cms.md) for content management details. --- ## Three Deployment Modes Explained ### 1. SUBDOMAIN MODE (Production - Recommended) **URL Pattern:** `https://STORE_SUBDOMAIN.platform-domain/` (root path = storefront) **Example:** - Store subdomain: `acme` - Platform domain: `omsflow.lu` - Customer Storefront URL: `https://acme.omsflow.lu/` - Product Catalog: `https://acme.omsflow.lu/products` - Staff Dashboard: `https://acme.omsflow.lu/store/dashboard` **How It Works:** 1. Customer visits `https://acme.omsflow.lu/products` 2. `PlatformContextMiddleware` detects subdomain `"acme"`, resolves platform from root domain `omsflow.lu` 3. Middleware rewrites path: `/products` → `/storefront/products` (internal) 4. `store_context_middleware` detects subdomain, queries: `SELECT * FROM stores WHERE subdomain = 'acme'` 5. Sets `request.state.store = Store(ACME Store)` 6. `frontend_type_middleware` detects STOREFRONT from `/storefront` path prefix 7. `theme_context_middleware` loads ACME's theme 8. Routes to storefront handler, renders with ACME's theme 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/` (root path = storefront) **Example:** - Store name: "ACME Store" - Custom domain: `store.acme-corp.com` - Customer Storefront URL: `https://store.acme-corp.com/products` **Database Setup:** ```sql -- 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/products` 2. `PlatformContextMiddleware` detects custom domain, resolves platform via `StoreDomain` lookup 3. Middleware rewrites path: `/products` → `/storefront/products` (internal) 4. `store_context_middleware` detects custom domain, queries `store_domains` table 5. Finds `StoreDomain` with `store_id = 1`, joins to get `Store(ACME Store)` 6. 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/storefront/STORE_CODE/...` **Example:** - Development: `http://localhost:8000/platforms/oms/storefront/ACME/products` - With port: `http://localhost:8000/platforms/loyalty/storefront/ACME/cart` **How It Works:** 1. Developer visits `http://localhost:8000/platforms/oms/storefront/ACME/products` 2. `PlatformContextMiddleware` detects `/platforms/oms/` prefix, sets platform context, strips prefix 3. `store_context_middleware` detects `/storefront/ACME/...` pattern, extracts store code `"ACME"` 4. Looks up Store: `SELECT * FROM stores WHERE store_code = 'ACME'` 5. Sets `request.state.store = Store(ACME)` 6. 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.omsflow.lu/ → Homepage https://acme.omsflow.lu/products → Product Catalog https://acme.omsflow.lu/products/123 → Product Detail https://acme.omsflow.lu/categories/electronics → Category Page https://acme.omsflow.lu/cart → Shopping Cart https://acme.omsflow.lu/checkout → Checkout https://acme.omsflow.lu/search?q=laptop → Search Results https://acme.omsflow.lu/account/login → Customer Login https://acme.omsflow.lu/account/dashboard → Account Dashboard (Auth Required) https://acme.omsflow.lu/account/orders → Order History (Auth Required) https://acme.omsflow.lu/store/dashboard → Staff Dashboard (Auth Required) ``` Note: In production, the root path `/` is the storefront. The `PlatformContextMiddleware` internally rewrites paths to `/storefront/` for route matching. Staff access is at `/store/`. ### Path-Based (DEVELOPMENT) ``` http://localhost:8000/platforms/oms/storefront/ACME/ → Homepage http://localhost:8000/platforms/oms/storefront/ACME/products → Products http://localhost:8000/platforms/oms/storefront/ACME/products/123 → Product Detail http://localhost:8000/platforms/oms/storefront/ACME/cart → Cart http://localhost:8000/platforms/oms/storefront/ACME/checkout → Checkout http://localhost:8000/platforms/oms/storefront/ACME/account/login → Login ``` ### API Endpoints (Same for All Modes) ``` GET /api/v1/storefront/products → Get store products (store from middleware) GET /api/v1/storefront/products/123 → Get product details POST /api/v1/storefront/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 ```python # Customer on acme.omsflow.lu tries to access TechPro's products # Store context is set to ACME by middleware — all queries scoped to ACME # 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: ```python # 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: ```html {{ request.state.store.name }}

Welcome to {{ request.state.store.name }}

``` --- ## 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 + Path Rewriting** The application handles routing by registering storefront routes **twice** with different prefixes: ```python # In main.py app.include_router(storefront_pages.router, prefix="/storefront") app.include_router(storefront_pages.router, prefix="/storefront/{store_code}") ``` **How This Works:** 1. **For Subdomain/Custom Domain Mode (Production):** - URL: `https://acme.omsflow.lu/products` - `PlatformContextMiddleware` detects subdomain, rewrites path: `/products` → `/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/storefront/ACME/products` - Platform middleware strips `/platforms/oms/` prefix, sets platform context - Matches: Second router with `/storefront/{store_code}` prefix - Route: `@router.get("/products")` → Full path: `/storefront/{store_code}/products` - Bonus: `store_code` available as path parameter! **Benefits:** - ✅ Clean separation: `/storefront/` = customer, `/store/` = staff - ✅ Production URLs are clean (root path = storefront) - ✅ No `/storefront/` prefix visible to production customers - ✅ Internal path rewriting handled by ASGI middleware - ✅ Both deployment modes supported cleanly --- ## Authentication in Multi-Tenant Storefront Customer authentication uses store-scoped cookies: ```python # 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/` | Production (standard) | *.platform.com | Add subdomains | | Custom Domain | `store-domain.com/` | Production (premium) | Per store | Store configures | | Path-Based | `localhost:8000/platforms/{p}/storefront/{v}/` | 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