# 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 = <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 /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 = <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'])
```
