# 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 https://STORE_SUBDOMAIN.platform-domain.lu/store/dashboard (staff) Example: https://acme.omsflow.lu/ https://acme.omsflow.lu/products https://techpro.rewardflow.lu/account/dashboard https://acme.omsflow.lu/store/dashboard (staff) Per-platform subdomain override: https://wizatech-rewards.rewardflow.lu/ (same store as wizatech.omsflow.lu) ``` ### 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 ``` --- ## Development URL Quick Reference All development URLs use `http://localhost:8000` as the base. ### Login Pages | Panel | URL | Description | |-------|-----|-------------| | Admin | `/admin/login` | Platform-wide admin panel | | Merchant | `/merchants/login` | Merchant management panel | | Store Dashboard | `/platforms/{platform_code}/store/{store_code}/login` | Store staff login | | Storefront | `/platforms/{platform_code}/storefront/{store_code}/account/login` | Customer login | ### Key Entry Points | Panel | URL | Description | |-------|-----|-------------| | Admin Dashboard | `/admin/` | Admin panel home | | Merchant Dashboard | `/merchants/dashboard` | Merchant panel home | | Store Dashboard | `/platforms/{platform_code}/store/{store_code}/dashboard` | Store management | | Storefront Homepage | `/platforms/{platform_code}/storefront/{store_code}/` | Customer-facing store | | Platform Homepage | `/platforms/{platform_code}/` | Platform marketing site | ### Full Example (OMS Platform, Store Code "ACME") ``` Admin login: http://localhost:8000/admin/login Merchant login: http://localhost:8000/merchants/login Store login: http://localhost:8000/platforms/oms/store/ACME/login Store dashboard: http://localhost:8000/platforms/oms/store/ACME/dashboard Storefront login: http://localhost:8000/platforms/oms/storefront/ACME/account/login Storefront homepage: http://localhost:8000/platforms/oms/storefront/ACME/ Storefront products: http://localhost:8000/platforms/oms/storefront/ACME/products Storefront cart: http://localhost:8000/platforms/oms/storefront/ACME/cart Storefront checkout: http://localhost:8000/platforms/oms/storefront/ACME/checkout Storefront account: http://localhost:8000/platforms/oms/storefront/ACME/account/dashboard ``` ### API Endpoints ``` Admin API: http://localhost:8000/api/v1/admin/... Store API: http://localhost:8000/api/v1/store/... Storefront API: http://localhost:8000/api/v1/storefront/... ``` ### Notes - **Admin and Merchant** panels are global — no platform prefix needed. - **Store and Storefront** panels require the `/platforms/{platform_code}/` prefix in development. This prefix is stripped by `PlatformContextMiddleware` before routing. - In **production**, storefronts are accessed via subdomain (`acme.omsflow.lu/`) or custom domain. The root path `/` is the storefront. - The storefront router is **double-mounted** at `/storefront/` and `/storefront/{store_code}/` to support both production and development modes transparently. --- ## 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` performs two-step subdomain lookup: - First: `SELECT * FROM store_platforms WHERE custom_subdomain = 'acme'` (per-platform override) - Fallback: `SELECT * FROM stores WHERE subdomain = 'acme'` (standard subdomain) 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) ``` Storefront (customer-facing): 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) Store Dashboard (staff): https://acme.omsflow.lu/store/dashboard → Staff Dashboard (Auth Required) https://acme.omsflow.lu/store/products → Manage Products https://acme.omsflow.lu/store/orders → Manage Orders https://acme.omsflow.lu/store/login → Staff Login Per-platform subdomain override: https://wizatech-rewards.rewardflow.lu/ → Same store as wizatech.omsflow.lu https://wizatech-rewards.rewardflow.lu/store/dashboard → Staff dashboard on loyalty platform ``` 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 both storefront and store dashboard routes **twice** with different prefixes: ```python # In main.py — Storefront routes (customer-facing) app.include_router(storefront_pages.router, prefix="/storefront") app.include_router(storefront_pages.router, prefix="/storefront/{store_code}") # In main.py — Store dashboard routes (staff management) app.include_router(store_pages.router, prefix="/store") app.include_router(store_pages.router, prefix="/store/{store_code}") ``` **How This Works:** 1. **For Subdomain/Custom Domain Mode (Production):** - Storefront: `https://acme.omsflow.lu/products` → path rewritten to `/storefront/products` → matches first storefront mount - Dashboard: `https://acme.omsflow.lu/store/dashboard` → matches first store mount at `/store` - Store resolved by middleware via `request.state.store` 2. **For Path-Based Development Mode:** - Storefront: `http://localhost:8000/platforms/oms/storefront/ACME/products` → matches second storefront mount at `/storefront/{store_code}` - Dashboard: `http://localhost:8000/platforms/oms/store/ACME/dashboard` → matches second store mount at `/store/{store_code}` - `store_code` available as path parameter ### `get_resolved_store_code` Dependency Route handlers use the `get_resolved_store_code` dependency to transparently obtain the store code regardless of deployment mode: ```python async def get_resolved_store_code(request: Request) -> str: # 1. Path parameter from double-mount (/store/{store_code}/...) store_code = request.path_params.get("store_code") if store_code: return store_code # 2. Middleware-resolved store (subdomain or custom domain) store = getattr(request.state, "store", None) if store: return store.store_code raise HTTPException(status_code=404, detail="Store not found") ``` **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 - ✅ `get_resolved_store_code` abstracts store resolution for handlers --- ## Per-Platform Subdomain Overrides Stores that are active on multiple platforms can have a **custom subdomain** per platform via `StorePlatform.custom_subdomain`. This allows a single store to appear under different subdomains on different platform domains. ### How It Works ``` Store: WizaTech (subdomain: "wizatech") ├── OMS platform → wizatech.omsflow.lu (uses Store.subdomain) └── Loyalty platform → wizatech-rewards.rewardflow.lu (uses StorePlatform.custom_subdomain) ``` **Database:** ```sql -- stores table id | store_code | subdomain 1 | WIZATECH | wizatech -- store_platforms table id | store_id | platform_id | custom_subdomain 1 | 1 | 1 (oms) | NULL -- uses store.subdomain = "wizatech" 2 | 1 | 2 (loyalty) | wizatech-rewards -- overrides to "wizatech-rewards" ``` **Resolution order** (in `store_context_middleware`): 1. Check `StorePlatform.custom_subdomain` for a match on the current platform 2. Fall back to `Store.subdomain` for the standard lookup ### Use Cases - **Brand differentiation**: A store selling electronics via OMS and running a loyalty program wants different branding per platform - **Subdomain conflicts**: Two unrelated stores might use the same subdomain on different platforms — custom subdomains resolve the collision - **Marketing**: Platform-specific landing URLs for campaigns (e.g., `wizatech-rewards.rewardflow.lu` for loyalty-specific promotions) --- ## 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: February 26, 2026 Orion Version: Current Development