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>
532 lines
20 KiB
Markdown
532 lines
20 KiB
Markdown
# 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 `/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](../multi-platform-cms.md) 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:**
|
|
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.py` → `storefront_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:**
|
|
```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/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
|
|
```python
|
|
# 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:
|
|
|
|
```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
|
|
<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:
|
|
|
|
```python
|
|
# 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:
|
|
|
|
```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/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
|