# Multi-Tenant System Complete guide to the multi-tenant architecture supporting custom domains, subdomains, and path-based routing. ## Overview The Wizamart platform supports **three deployment modes** for multi-tenancy, allowing each vendor to have their own isolated shop while sharing the same application instance and database. **Key Concept**: One application, multiple isolated vendor shops, each accessible via different URLs. **Important Distinction:** - **Vendor Dashboard** (all modes): `/vendor/{code}/*` (singular) - Management interface for vendors - **Shop Storefront** (path-based only): `/vendors/{code}/shop/*` (plural) - Customer-facing shop - This naming distinction helps separate administrative routes from public-facing shop routes ## The Three Routing Modes ### 1. Custom Domain Mode **Concept**: Each vendor has their own domain pointing to the platform. **Example**: ``` customdomain1.com → Vendor 1 Shop anothershop.com → Vendor 2 Shop beststore.net → Vendor 3 Shop ``` **How it works**: 1. Vendor registers a custom domain 2. Domain's DNS is configured to point to the platform 3. Platform detects vendor by matching domain in database 4. Vendor's shop is displayed with their theme/branding **Use Case**: Professional vendors who want their own branded domain **Configuration**: ```python # Database: vendor_domains table vendor_id | domain ----------|------------------ 1 | customdomain1.com 2 | anothershop.com 3 | beststore.net ``` ### 2. Subdomain Mode **Concept**: Each vendor gets a subdomain of the platform domain. **Example**: ``` vendor1.platform.com → Vendor 1 Shop vendor2.platform.com → Vendor 2 Shop vendor3.platform.com → Vendor 3 Shop admin.platform.com → Admin Interface ``` **How it works**: 1. Vendor is assigned a unique code (e.g., "vendor1") 2. Subdomain is automatically available: `{code}.platform.com` 3. Platform detects vendor from subdomain prefix 4. No DNS configuration needed by vendor **Use Case**: Easy setup, no custom domain required **Configuration**: ```python # Vendors table id | code | name ---|---------|---------- 1 | vendor1 | Vendor One Shop 2 | vendor2 | Vendor Two Shop 3 | vendor3 | Vendor Three Shop ``` ### 3. Path-Based Mode **Concept**: All vendors share the same domain, differentiated by URL path. **Example**: ``` platform.com/vendors/vendor1/shop → Vendor 1 Shop platform.com/vendors/vendor2/shop → Vendor 2 Shop platform.com/vendors/vendor3/shop → Vendor 3 Shop ``` **How it works**: 1. URL path includes vendor code 2. Platform extracts vendor code from path 3. Path is rewritten for routing 4. All vendors on same domain **Use Case**: Development and testing environments only **Path Patterns**: - `/vendors/{code}/shop/*` - Storefront pages (correct pattern) - `/vendor/{code}/*` - Vendor dashboard pages (different context) ## Routing Mode Comparison | Feature | Custom Domain | Subdomain | Path-Based | |---------|---------------|-----------|------------| | **Professionalism** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | | **Setup Complexity** | High (DNS required) | Low (automatic) | Very Low | | **SSL Complexity** | Medium (wildcard or per-domain) | Low (wildcard SSL) | Very Low (single cert) | | **SEO Benefits** | Best (own domain) | Good | Limited | | **Cost** | High (domain + SSL) | Low (wildcard SSL) | Lowest | | **Isolation** | Best (separate domain) | Good | Good | | **URL Appearance** | `shop.com` | `shop.platform.com` | `platform.com/vendor/shop` | ## Implementation Details ### Vendor Detection Logic The `VendorContextMiddleware` detects vendors using this priority: ```python def detect_vendor(request): host = request.headers.get("host") # 1. Try custom domain first vendor = find_by_custom_domain(host) if vendor: return vendor, "custom_domain" # 2. Try subdomain if host != settings.platform_domain: vendor_code = host.split('.')[0] vendor = find_by_code(vendor_code) if vendor: return vendor, "subdomain" # 3. Try path-based path = request.url.path if path.startswith("/vendor/") or path.startswith("/vendors/"): vendor_code = extract_code_from_path(path) vendor = find_by_code(vendor_code) if vendor: return vendor, "path_based" return None, None ``` ### Path Extraction For path-based routing, clean paths are extracted: **Path-Based Shop Routes** (Development): ``` Original: /vendors/WIZAMART/shop/products Extracted: vendor_code = "WIZAMART" Clean: /shop/products ``` **Vendor Dashboard Routes** (All environments): ``` Original: /vendor/WIZAMART/dashboard Extracted: vendor_code = "WIZAMART" Clean: /dashboard ``` **Note**: The shop storefront uses `/vendors/` (plural) while the vendor dashboard uses `/vendor/` (singular). This distinction helps separate customer-facing shop routes from vendor management routes. **Why Clean Path?** - FastAPI routes don't include vendor prefix - Routes defined as: `@app.get("/shop/products")` - Path must be rewritten to match routes ## Database Schema ### Vendors Table ```sql CREATE TABLE vendors ( id SERIAL PRIMARY KEY, code VARCHAR(50) UNIQUE NOT NULL, -- For subdomain/path routing name VARCHAR(255) NOT NULL, is_active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT NOW() ); ``` ### Vendor Domains Table ```sql CREATE TABLE vendor_domains ( id SERIAL PRIMARY KEY, vendor_id INTEGER REFERENCES vendors(id), domain VARCHAR(255) UNIQUE NOT NULL, -- Custom domain is_verified BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT NOW() ); ``` **Example Data**: ```sql -- Vendors INSERT INTO vendors (code, name) VALUES ('wizamart', 'Wizamart Shop'), ('techstore', 'Tech Store'), ('fashionhub', 'Fashion Hub'); -- Custom Domains INSERT INTO vendor_domains (vendor_id, domain) VALUES (1, 'wizamart.com'), (2, 'mytechstore.net'); ``` ## Deployment Scenarios ### Scenario 1: Small Platform (Path-Based) **Setup**: - Single domain: `myplatform.com` - All vendors use path-based routing - Single SSL certificate - Simplest infrastructure **URLs**: ``` myplatform.com/admin myplatform.com/vendors/shop1/shop myplatform.com/vendors/shop2/shop myplatform.com/vendors/shop3/shop ``` **Infrastructure**: ``` [Internet] → [Single Server] → [PostgreSQL] myplatform.com ``` ### Scenario 2: Medium Platform (Subdomain) **Setup**: - Main domain: `myplatform.com` - Vendors get subdomains automatically - Wildcard SSL certificate (`*.myplatform.com`) - Better branding for vendors **URLs**: ``` admin.myplatform.com shop1.myplatform.com shop2.myplatform.com shop3.myplatform.com ``` **Infrastructure**: ``` [Internet] → [Load Balancer] → [App Servers] → [PostgreSQL] *.myplatform.com ``` ### Scenario 3: Large Platform (Mixed Mode) **Setup**: - Supports all three modes - Premium vendors get custom domains - Regular vendors use subdomains - Free tier uses path-based **URLs**: ``` # Custom domains (premium) customdomain.com → Vendor 1 anotherdomain.com → Vendor 2 # Subdomains (standard) shop3.myplatform.com → Vendor 3 shop4.myplatform.com → Vendor 4 # Path-based (free tier) myplatform.com/vendors/shop5/shop → Vendor 5 myplatform.com/vendors/shop6/shop → Vendor 6 ``` **Infrastructure**: ``` [CDN/Load Balancer] | +-----------------+------------------+ | | | [App Server 1] [App Server 2] [App Server 3] | | | +-----------------+------------------+ | [PostgreSQL Cluster] ``` ## DNS Configuration ### For Custom Domains **Vendor Side**: ``` # DNS A Record customdomain.com. A 203.0.113.10 (platform IP) # Or CNAME customdomain.com. CNAME myplatform.com. ``` **Platform Side**: - Add domain to `vendor_domains` table - Generate SSL certificate (Let's Encrypt) - Verify domain ownership ### For Subdomains **Platform Side**: ``` # Wildcard DNS *.myplatform.com. A 203.0.113.10 # Or individual subdomains shop1.myplatform.com. A 203.0.113.10 shop2.myplatform.com. A 203.0.113.10 ``` **SSL Certificate**: ```bash # Wildcard certificate *.myplatform.com myplatform.com ``` ## Tenant Isolation ### Data Isolation Every database query is scoped to `vendor_id`: ```python # Example: Get products for current vendor products = db.query(Product).filter( Product.vendor_id == request.state.vendor_id ).all() # Example: Create order for vendor order = Order( vendor_id=request.state.vendor_id, customer_id=customer_id, # ... other fields ) ``` **Critical**: ALWAYS filter by `vendor_id` in queries! ### Theme Isolation Each vendor has independent theme settings: ```python # Vendor 1 theme { "primary_color": "#3B82F6", "logo_url": "/static/vendors/vendor1/logo.png" } # Vendor 2 theme { "primary_color": "#10B981", "logo_url": "/static/vendors/vendor2/logo.png" } ``` ### File Storage Isolation Vendor files stored in separate directories: ``` static/ └── vendors/ ├── vendor1/ │ ├── logo.png │ ├── favicon.ico │ └── products/ │ ├── product1.jpg │ └── product2.jpg └── vendor2/ ├── logo.png └── products/ └── product1.jpg ``` ## Request Examples ### Example 1: Custom Domain Request **Request**: ```http GET /shop/products HTTP/1.1 Host: customdomain.com ``` **Processing**: ``` 1. VendorContextMiddleware - Checks: domain = "customdomain.com" - Queries: vendor_domains WHERE domain = "customdomain.com" - Finds: vendor_id = 1 - Sets: request.state.vendor = 2. ContextDetectionMiddleware - Analyzes: path = "/shop/products" - Sets: context_type = SHOP 3. ThemeContextMiddleware - Queries: vendor_themes WHERE vendor_id = 1 - Sets: request.state.theme = {...} 4. Route Handler - Queries: products WHERE vendor_id = 1 - Renders: template with Vendor 1 theme ``` ### Example 2: Subdomain Request **Request**: ```http GET /shop/products HTTP/1.1 Host: wizamart.myplatform.com ``` **Processing**: ``` 1. VendorContextMiddleware - Checks: host != "myplatform.com" - Extracts: subdomain = "wizamart" - Queries: vendors WHERE code = "wizamart" - Sets: request.state.vendor = 2-4. Same as Example 1 ``` ### Example 3: Path-Based Request **Request**: ```http GET /vendors/WIZAMART/shop/products HTTP/1.1 Host: myplatform.com ``` **Processing**: ``` 1. VendorContextMiddleware - Checks: path starts with "/vendor/" - Extracts: code = "WIZAMART" - Queries: vendors WHERE code = "WIZAMART" - Sets: request.state.vendor = - Sets: request.state.clean_path = "/shop/products" 2. FastAPI Router - Routes registered with prefix: /vendors/{vendor_code}/shop - Matches: /vendors/WIZAMART/shop/products - vendor_code path parameter = "WIZAMART" 3-4. Same as previous examples (Context, Theme middleware) ``` ## Testing Multi-Tenancy ### Unit Tests ```python def test_vendor_detection_custom_domain(): request = MockRequest(host="customdomain.com") middleware = VendorContextMiddleware() vendor, mode = middleware.detect_vendor(request, db) assert vendor.code == "vendor1" assert mode == "custom_domain" def test_vendor_detection_subdomain(): request = MockRequest(host="shop1.platform.com") middleware = VendorContextMiddleware() vendor, mode = middleware.detect_vendor(request, db) assert vendor.code == "shop1" assert mode == "subdomain" ``` ### Integration Tests ```python def test_shop_page_multi_tenant(client): # Test subdomain routing response = client.get( "/shop/products", headers={"Host": "wizamart.platform.com"} ) assert "Wizamart" in response.text # Test different vendor response = client.get( "/shop/products", headers={"Host": "techstore.platform.com"} ) assert "Tech Store" in response.text ``` ## Security Considerations ### 1. Tenant Isolation **Always scope queries**: ```python # ✅ Good - Scoped to vendor products = db.query(Product).filter( Product.vendor_id == request.state.vendor_id ).all() # ❌ Bad - Not scoped, leaks data across tenants! products = db.query(Product).all() ``` ### 2. Domain Verification Before activating custom domain: 1. Verify DNS points to platform 2. Check domain ownership (email/file verification) 3. Generate SSL certificate 4. Mark domain as verified ### 3. Input Validation Validate vendor codes: ```python # Sanitize vendor code vendor_code = vendor_code.lower().strip() # Validate format if not re.match(r'^[a-z0-9-]{3,50}$', vendor_code): raise ValidationError("Invalid vendor code") ``` ## Performance Optimization ### 1. Cache Vendor Lookups ```python # Cache vendor by domain/code @lru_cache(maxsize=1000) def get_vendor_by_code(code: str): return db.query(Vendor).filter(Vendor.code == code).first() ``` ### 2. Database Indexes ```sql -- Index for fast lookups CREATE INDEX idx_vendors_code ON vendors(code); CREATE INDEX idx_vendor_domains_domain ON vendor_domains(domain); CREATE INDEX idx_products_vendor_id ON products(vendor_id); ``` ### 3. Connection Pooling Ensure database connection pool is properly configured: ```python # sqlalchemy engine engine = create_engine( DATABASE_URL, pool_size=20, max_overflow=40, pool_pre_ping=True ) ``` ## Related Documentation - [Middleware Stack](middleware.md) - How vendor detection works - [Request Flow](request-flow.md) - Complete request journey - [Architecture Overview](overview.md) - System architecture - [Authentication & RBAC](auth-rbac.md) - Multi-tenant security ## Migration Guide ### Adding Multi-Tenancy to Existing Tables ```python # Alembic migration def upgrade(): # Add vendor_id to existing table op.add_column('products', sa.Column('vendor_id', sa.Integer(), nullable=True) ) # Set default vendor for existing data op.execute("UPDATE products SET vendor_id = 1 WHERE vendor_id IS NULL") # Make non-nullable op.alter_column('products', 'vendor_id', nullable=False) # Add foreign key op.create_foreign_key( 'fk_products_vendor', 'products', 'vendors', ['vendor_id'], ['id'] ) # Add index op.create_index('idx_products_vendor_id', 'products', ['vendor_id']) ```