Multi-Tenant Architecture¶
FireBreak's multi-tenant architecture provides complete isolation for enterprise clients while sharing the same codebase and infrastructure.
Overview¶
Multi-tenancy in FireBreak allows:
- Multiple clients to use the same platform
- Complete data isolation between tenants
- Per-tenant branding and customization
- Independent scaling and resource limits
- Tenant-specific feature flags
Architecture Diagram¶
graph TB
subgraph "User Layer"
U1[Tenant A User]
U2[Tenant B User]
U3[Tenant C User]
end
subgraph "Routing Layer"
R[DNS Routing<br/>subdomain.firebreakrisk.com]
end
subgraph "Frontend Layer"
FA[Tenant A Frontend<br/>acme.firebreakrisk.com]
FB[Tenant B Frontend<br/>wildguard.firebreakrisk.com]
FC[Tenant C Frontend<br/>guardian.firebreakrisk.com]
end
subgraph "Backend Layer"
API[Shared Backend API<br/>api.firebreakrisk.com]
end
subgraph "Storage Layer - Isolated"
DA[(Tenant A Database)]
DB[(Tenant B Database)]
DC[(Tenant C Database)]
SA[Tenant A R2 Bucket]
SB[Tenant B R2 Bucket]
SC[Tenant C R2 Bucket]
end
U1 --> R
U2 --> R
U3 --> R
R --> FA
R --> FB
R --> FC
FA --> API
FB --> API
FC --> API
API --> DA
API --> DB
API --> DC
API --> SA
API --> SB
API --> SC Tenant Isolation Levels¶
Level 1: Subdomain Isolation¶
Each tenant gets a unique subdomain:
acme.firebreakrisk.com → Tenant A
wildguard.firebreakrisk.com → Tenant B
guardian.firebreakrisk.com → Tenant C
Level 2: Frontend Isolation¶
Each tenant has a dedicated frontend deployment with:
- Custom branding (colors, logos, fonts)
- Tenant-specific UI components
- White-label experience
- Independent CSS/JS bundles
Level 3: Data Isolation¶
Complete data separation:
- Separate D1 Databases: Each tenant has dedicated database
- Separate R2 Buckets: Isolated photo/video storage
- Separate KV Namespaces: Tenant-specific caching
- Tenant ID in all queries: Runtime validation
Level 4: Resource Isolation¶
Per-tenant limits:
- API rate limiting
- Storage quotas
- Concurrent request limits
- CPU/memory constraints
Tenant Configuration¶
Tenant Config File¶
{
"tenant_id": "acme-insurance",
"name": "ACME Insurance",
"subdomain": "acme",
"status": "active",
"created_at": "2025-01-15",
"branding": {
"primary_color": "#FF6600",
"secondary_color": "#003366",
"logo_url": "https://r2.firebreakrisk.com/acme/logo.png",
"favicon_url": "https://r2.firebreakrisk.com/acme/favicon.ico",
"font_family": "Inter, sans-serif"
},
"features": {
"multi_model_consensus": true,
"video_analysis": true,
"custom_reports": true,
"api_access": true
},
"limits": {
"max_users": 100,
"max_assessments_per_month": 1000,
"max_storage_gb": 50,
"api_rate_limit_per_minute": 100
},
"storage": {
"d1_database_id": "tenant-acme-db",
"r2_bucket": "firestorm-photos-acme",
"kv_namespace_id": "tenant-acme-cache"
},
"contact": {
"admin_email": "admin@acme-insurance.com",
"billing_email": "billing@acme-insurance.com",
"support_email": "support@acme-insurance.com"
}
}
Tenant Registry¶
All tenants registered in tenants/registry.json:
{
"tenants": [
{
"id": "acme-insurance",
"subdomain": "acme",
"status": "active"
},
{
"id": "wildguard-insurance",
"subdomain": "wildguard",
"status": "active"
},
{
"id": "property-guardian",
"subdomain": "guardian",
"status": "trial"
}
]
}
Request Flow¶
1. User Request¶
2. DNS Routing¶
3. Frontend Serving¶
Cloudflare Pages serves tenant-specific frontend:
- Load acme branding
- Inject tenant ID in API calls
- Apply custom CSS theme
4. API Request¶
// Frontend makes API call
fetch('https://api.firebreakrisk.com/api/v1/analyze', {
headers: {
'X-Tenant-ID': 'acme-insurance',
'Authorization': 'Bearer ...'
}
})
5. Backend Processing¶
# Backend validates tenant
async def handle_request(request, env):
tenant_id = request.headers.get('X-Tenant-ID')
# Validate tenant exists
tenant = await get_tenant(tenant_id, env)
if not tenant:
return Response('Invalid tenant', status=403)
# Use tenant-specific database
db = env.get_database(tenant.database_id)
# Process request with tenant context
result = await process_with_tenant(request, tenant, db)
return result
Tenant Provisioning¶
Automated Provisioning¶
# Using CLI tool
cd infrastructure/scripts
./provision-tenant.sh acme-insurance
# Steps performed:
# 1. Create D1 database
# 2. Create R2 bucket
# 3. Create KV namespace
# 4. Generate tenant config
# 5. Deploy frontend with branding
# 6. Set up DNS records
# 7. Send welcome email
Manual Provisioning¶
See Tenant Provisioning Guide for detailed steps.
Security Considerations¶
Tenant ID Validation¶
def validate_tenant_access(request, tenant_id, env):
"""Ensure user can only access their tenant's data"""
# Extract tenant from request
request_tenant = request.headers.get('X-Tenant-ID')
# Validate match
if request_tenant != tenant_id:
raise ForbiddenError("Tenant ID mismatch")
# Validate tenant exists and is active
tenant = get_tenant(tenant_id, env)
if not tenant or tenant.status != 'active':
raise ForbiddenError("Invalid or inactive tenant")
return tenant
Data Isolation Checks¶
async def get_assessment(assessment_id, tenant_id, env):
"""Fetch assessment with tenant validation"""
# Get tenant database
db = env.get_database(f"tenant-{tenant_id}-db")
# Query with tenant_id in WHERE clause
result = await db.execute(
"SELECT * FROM assessments WHERE id = ? AND tenant_id = ?",
[assessment_id, tenant_id]
)
return result.first()
Rate Limiting Per Tenant¶
async def check_rate_limit(tenant_id, env):
"""Check if tenant has exceeded rate limit"""
# Get tenant config
tenant = get_tenant(tenant_id, env)
limit = tenant.limits.api_rate_limit_per_minute
# Check current usage
key = f"rate:{tenant_id}:{current_minute}"
current = await env.CACHE.get(key) or 0
if current >= limit:
raise RateLimitError(f"Exceeded {limit} requests per minute")
# Increment counter
await env.CACHE.put(key, current + 1, expiration=60)
Tenant Lifecycle¶
stateDiagram-v2
[*] --> Trial: New Signup
Trial --> Active: Payment Confirmed
Trial --> Suspended: Trial Expired
Active --> Suspended: Payment Failed
Suspended --> Active: Payment Resumed
Active --> Cancelled: User Cancels
Suspended --> Cancelled: Grace Period Expired
Cancelled --> [*]: Data Retention Period Ends Status Definitions¶
- Trial: 30-day free trial, limited features
- Active: Paid subscription, full features
- Suspended: Payment issue, read-only access
- Cancelled: Subscription ended, data retention for 90 days
Scaling Considerations¶
Horizontal Scaling¶
- Workers: Auto-scale per request
- Databases: D1 auto-scales reads
- Storage: R2 unlimited capacity
- CDN: Cloudflare global network
Performance Optimization¶
- Per-tenant caching: KV namespace per tenant
- CDN caching: Cloudflare cache per subdomain
- Database indexing: Tenant-specific indexes
- Lazy loading: Load branding on-demand
Monitoring & Analytics¶
Per-Tenant Metrics¶
// Track metrics per tenant
await env.ANALYTICS.write({
tenant_id: 'acme-insurance',
metric: 'api_request',
endpoint: '/api/v1/analyze',
duration_ms: 245,
status: 200,
timestamp: new Date()
})
Tenant Dashboard¶
Each tenant can view:
- API usage statistics
- Storage consumption
- Active users
- Assessment counts
- Error rates
Migration & Onboarding¶
Migrating Existing Data¶
# Import existing data for new tenant
./scripts/import-tenant-data.sh \
--tenant acme-insurance \
--data-file acme_export.json \
--validate
Tenant Onboarding Checklist¶
- Create tenant configuration
- Provision infrastructure (D1, R2, KV)
- Set up DNS records
- Deploy branded frontend
- Import existing data (if applicable)
- Set up user accounts
- Configure API keys
- Test end-to-end workflows
- Enable monitoring
- Send welcome email and documentation
Reference Documentation¶
For detailed implementation: