revamping documentation
This commit is contained in:
601
docs/architecture/multi-tenant.md
Normal file
601
docs/architecture/multi-tenant.md
Normal file
@@ -0,0 +1,601 @@
|
||||
# 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.
|
||||
|
||||
## 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/vendor/vendor1/shop → Vendor 1 Shop
|
||||
platform.com/vendor/vendor2/shop → Vendor 2 Shop
|
||||
platform.com/vendors/vendor3/shop → Vendor 3 Shop (alternative)
|
||||
```
|
||||
|
||||
**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**: Simplest deployment, single domain certificate
|
||||
|
||||
**Path Patterns**:
|
||||
- `/vendor/{code}/shop/*` - Storefront pages
|
||||
- `/vendor/{code}/api/*` - API endpoints (if needed)
|
||||
- `/vendors/{code}/shop/*` - Alternative pattern
|
||||
|
||||
## 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:
|
||||
|
||||
**Example 1**: Single vendor prefix
|
||||
```
|
||||
Original: /vendor/WIZAMART/shop/products
|
||||
Extracted: vendor_code = "WIZAMART"
|
||||
Clean: /shop/products
|
||||
```
|
||||
|
||||
**Example 2**: Plural vendors prefix
|
||||
```
|
||||
Original: /vendors/WIZAMART/shop/products
|
||||
Extracted: vendor_code = "WIZAMART"
|
||||
Clean: /shop/products
|
||||
```
|
||||
|
||||
**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/vendor/shop1/shop
|
||||
myplatform.com/vendor/shop2/shop
|
||||
myplatform.com/vendor/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/vendor/shop5/shop → Vendor 5
|
||||
myplatform.com/vendor/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 = <Vendor 1>
|
||||
|
||||
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 = <Vendor "wizamart">
|
||||
|
||||
2-4. Same as Example 1
|
||||
```
|
||||
|
||||
### Example 3: Path-Based Request
|
||||
|
||||
**Request**:
|
||||
```http
|
||||
GET /vendor/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 = <Vendor>
|
||||
- Sets: request.state.clean_path = "/shop/products"
|
||||
|
||||
2. PathRewriteMiddleware
|
||||
- Rewrites: request.scope['path'] = "/shop/products"
|
||||
|
||||
3-4. Same as previous examples
|
||||
```
|
||||
|
||||
## 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'])
|
||||
```
|
||||
Reference in New Issue
Block a user