16 KiB
Wizamart Multi-Tenant URL Routing Guide
Quick Answer
How do customers access a vendor's shop in Wizamart?
There are three ways depending on the deployment mode:
⚠️ Important: This guide describes customer-facing shop routes. For vendor dashboard/management routes, see Vendor Dashboard Documentation. The shop uses /vendors/{code}/shop/* (plural) in path-based mode, while the vendor dashboard uses /vendor/{code}/* (singular).
1. SUBDOMAIN MODE (Production - Recommended)
https://VENDOR_SUBDOMAIN.platform.com/shop/products
Example:
https://acme.wizamart.com/shop/products
https://techpro.wizamart.com/shop/categories/electronics
2. CUSTOM DOMAIN MODE (Production - Premium)
https://VENDOR_CUSTOM_DOMAIN/shop/products
Example:
https://store.acmecorp.com/shop/products
https://shop.techpro.io/shop/cart
3. PATH-BASED MODE (Development Only)
http://localhost:PORT/vendors/VENDOR_CODE/shop/products
Example:
http://localhost:8000/vendors/acme/shop/products
http://localhost:8000/vendors/techpro/shop/checkout
Three Deployment Modes Explained
1. SUBDOMAIN MODE (Production - Recommended)
URL Pattern: https://VENDOR_SUBDOMAIN.platform.com/shop/...
Example:
- Vendor subdomain:
acme - Platform domain:
wizamart.com - Customer Shop URL:
https://acme.wizamart.com/shop/products - Product Detail:
https://acme.wizamart.com/shop/products/123
How It Works:
- Customer visits
https://acme.wizamart.com/shop/products vendor_context_middlewaredetects subdomain"acme"- Queries:
SELECT * FROM vendors WHERE subdomain = 'acme' - Finds Vendor with ID=1 (ACME Store)
- Sets
request.state.vendor = Vendor(ACME Store) context_middlewaredetects it's a SHOP requesttheme_context_middlewareloads ACME's theme- Routes to
shop_pages.py→shop_products_page() - Renders template with ACME's colors, logo, and products
Advantages:
- Single SSL certificate for all vendors (*.wizamart.com)
- 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/shop/...
Example:
- Vendor name: "ACME Store"
- Custom domain:
store.acme-corp.com - Customer Shop URL:
https://store.acme-corp.com/shop/products
Database Setup:
-- vendors table
id | name | subdomain
1 | ACME Store | acme
-- vendor_domains table (links custom domains to vendors)
id | vendor_id | domain | is_active | is_verified
1 | 1 | store.acme-corp.com | true | true
How It Works:
- Customer visits
https://store.acme-corp.com/shop/products vendor_context_middlewaredetects custom domain (not *.wizamart.com, not localhost)- Normalizes domain to
"store.acme-corp.com" - Queries:
SELECT * FROM vendor_domains WHERE domain = 'store.acme-corp.com' - Finds
VendorDomainwithvendor_id = 1 - Joins to get
Vendor(ACME Store) - Rest is same as subdomain mode...
Advantages:
- Professional branding with vendor's own domain
- Better for premium vendors
- Vendor controls the domain
Considerations:
- Each vendor needs their own SSL certificate
- Vendor must own and configure the domain
3. PATH-BASED MODE (Development Only)
URL Pattern: http://localhost:PORT/vendors/VENDOR_CODE/shop/...
Example:
- Development:
http://localhost:8000/vendors/acme/shop/products - With port:
http://localhost:8000/vendors/acme/shop/products/123
How It Works:
- Developer visits
http://localhost:8000/vendors/acme/shop/products vendor_context_middlewaredetects path-based routing pattern/vendors/acme/...- Extracts vendor code
"acme"from the path - Looks up Vendor:
SELECT * FROM vendors WHERE subdomain = 'acme' - Sets
request.state.vendor = Vendor(acme) - Routes to shop pages
Advantages:
- Perfect for local development
- No need to configure DNS/domains
- Test multiple vendors easily without domain setup
Limitations:
- Only for development (not production-ready)
- All vendors share same localhost address
Complete Route Examples
Subdomain/Custom Domain (PRODUCTION)
https://acme.wizamart.com/shop/ → Homepage
https://acme.wizamart.com/shop/products → Product Catalog
https://acme.wizamart.com/shop/products/123 → Product Detail
https://acme.wizamart.com/shop/categories/electronics → Category Page
https://acme.wizamart.com/shop/cart → Shopping Cart
https://acme.wizamart.com/shop/checkout → Checkout
https://acme.wizamart.com/shop/search?q=laptop → Search Results
https://acme.wizamart.com/shop/account/login → Customer Login
https://acme.wizamart.com/shop/account/dashboard → Account Dashboard (Auth Required)
https://acme.wizamart.com/shop/account/orders → Order History (Auth Required)
https://acme.wizamart.com/shop/account/profile → Profile (Auth Required)
Path-Based (DEVELOPMENT)
http://localhost:8000/vendors/acme/shop/ → Homepage
http://localhost:8000/vendors/acme/shop/products → Products
http://localhost:8000/vendors/acme/shop/products/123 → Product Detail
http://localhost:8000/vendors/acme/shop/cart → Cart
http://localhost:8000/vendors/acme/shop/checkout → Checkout
http://localhost:8000/vendors/acme/shop/account/login → Login
API Endpoints (Same for All Modes)
GET /api/v1/public/vendors/1/products → Get vendor products
GET /api/v1/public/vendors/1/products/123 → Get product details
POST /api/v1/public/vendors/1/products/{id}/reviews → Add product review
How Vendor Isolation Works
Multi-Layer Enforcement
Layer 1: URL Routing
- Vendor is detected from subdomain, custom domain, or path
- Each vendor gets their own request context
Layer 2: Middleware
request.state.vendoris set to the detected Vendor object- All downstream code can access the vendor
Layer 3: Database Queries
- All queries must include
WHERE vendor_id = ? - Product queries:
SELECT * FROM products WHERE vendor_id = 1 - Order queries:
SELECT * FROM orders WHERE vendor_id = 1
Layer 4: API Authorization
- Endpoints verify the vendor matches the request vendor
- Customers can only see their own vendor's products
Example: No Cross-Vendor Leakage
# Customer on acme.wizamart.com tries to access TechPro's products
# They make API call to /api/v1/public/vendors/2/products
# Backend checks:
vendor = get_vendor_from_request(request) # Returns Vendor(id=1, name="ACME")
if vendor.id != requested_vendor_id: # if 1 != 2
raise UnauthorizedShopAccessException()
Request Lifecycle: Complete Flow
Scenario: Customer visits https://acme.wizamart.com/shop/products
┌─────────────────────────────────────────────────────────────────┐
│ 1. REQUEST ARRIVES │
└─────────────────────────────────────────────────────────────────┘
method: GET
host: acme.wizamart.com
path: /shop/products
┌─────────────────────────────────────────────────────────────────┐
│ 2. MIDDLEWARE CHAIN │
└─────────────────────────────────────────────────────────────────┘
A) vendor_context_middleware
├─ Detects host: "acme.wizamart.com"
├─ Extracts subdomain: "acme"
├─ Queries: SELECT * FROM vendors WHERE subdomain = 'acme'
└─ Sets: request.state.vendor = Vendor(ACME Store)
B) context_middleware
├─ Checks path: "/shop/products"
├─ Has request.state.vendor? YES
└─ Sets: request.state.context_type = RequestContext.SHOP
C) theme_context_middleware
├─ Queries: SELECT * FROM vendor_themes WHERE vendor_id = 1
└─ Sets: request.state.theme = {...ACME's theme...}
┌─────────────────────────────────────────────────────────────────┐
│ 3. ROUTE MATCHING │
└─────────────────────────────────────────────────────────────────┘
Path: /shop/products
Matches: @router.get("/shop/products")
Handler: shop_products_page(request)
┌─────────────────────────────────────────────────────────────────┐
│ 4. HANDLER EXECUTES │
└─────────────────────────────────────────────────────────────────┘
@router.get("/shop/products", response_class=HTMLResponse)
async def shop_products_page(request: Request):
return templates.TemplateResponse(
"shop/products.html",
{"request": request}
)
┌─────────────────────────────────────────────────────────────────┐
│ 5. TEMPLATE RENDERS │
└─────────────────────────────────────────────────────────────────┘
Template accesses:
├─ request.state.vendor.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/public/vendors/1/products`)
.then(data => renderProducts(data.products, {theme}))
┌─────────────────────────────────────────────────────────────────┐
│ 7. RESPONSE SENT │
└─────────────────────────────────────────────────────────────────┘
HTML with ACME's colors, logo, and products
Theme Integration
Each vendor's shop is fully branded with their custom theme:
# Theme loaded for https://acme.wizamart.com
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.vendor.name }}" />
<h1 style="font-family: {{ request.state.theme.fonts.heading }}">
Welcome to {{ request.state.vendor.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. Vendor Perspective
- Vendors can use a subdomain (free/standard):
acme.wizamart.com - 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 vendor is being accessed
- All business logic remains vendor-unaware
- Database queries automatically filtered by vendor
- 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 vendor-scoped cookies
- Database: All tables have
vendor_idforeign key
Potential Issue: Path-Based Development Mode
⚠️ Current Implementation Gap:
The vendor_context_middleware sets clean_path for path-based URLs, but this isn't used for FastAPI routing.
Problem:
- Incoming:
GET http://localhost:8000/vendors/acme/shop/products - Routes registered:
@router.get("/shop/products") - FastAPI tries to match
/vendors/acme/shop/productsagainst/shop/products - Result: ❌ 404 Not Found
Solution (Recommended):
Add a path rewriting middleware in main.py:
async def path_rewrite_middleware(request: Request, call_next):
"""Rewrite path for path-based vendor routing in development mode."""
if hasattr(request.state, 'clean_path'):
# Replace request path for FastAPI routing
request._url = request._url.replace(path=request.state.clean_path)
return await call_next(request)
# In main.py, add after vendor_context_middleware:
app.middleware("http")(path_rewrite_middleware)
Or alternatively, mount the router twice:
app.include_router(shop_pages.router, prefix="/shop")
app.include_router(shop_pages.router, prefix="/vendors/{vendor_code}/shop") # Path-based development mode
Authentication in Multi-Tenant Shop
Customer authentication uses vendor-scoped cookies:
# Login sets cookie scoped to vendor's shop
Set-Cookie: customer_token=eyJ...; Path=/shop; HttpOnly; SameSite=Lax
# This prevents:
# - Tokens leaking across vendors
# - Cross-site request forgery
# - Cookie scope confusion in multi-tenant setup
Summary Table
| Mode | URL | Use Case | SSL | DNS |
|---|---|---|---|---|
| Subdomain | vendor.platform.com/shop |
Production (standard) | *.platform.com | Add subdomains |
| Custom Domain | vendor-domain.com/shop |
Production (premium) | Per vendor | Vendor configures |
| Path-Based | localhost:8000/vendors/v/shop |
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 vendors with different themes
- For Scaling: Consider CDN for vendor-specific assets
Generated: November 7, 2025 Wizamart Version: Current Development