Multi-Tenancy
Implementation guide for multi-tenancy including X-Tenant-ID header, middleware, RBAC helpers, and query filtering
MQUAD uses a hybrid multi-tenancy model with subdomain-based identification for users and header-based isolation for service-to-service communication.
Tenant Identification
Subdomain (User-Facing)
https://acme.mquad.local/api/v1/users
└── subdomainHeader (Service-to-Service)
X-Tenant-ID: <tenant-uuid>Tenant Middleware
The tenant middleware must be first in the FastAPI middleware stack:
# backend/api/interfaces/middlewares/tenant_middleware.py
app.add_middleware(TenantMiddleware) # FIRST
app.add_middleware(AuthMiddleware) # SECOND
app.add_middleware(CORSMiddleware) # THIRDMiddleware implementation:
class TenantMiddleware:
async def __call__(self, request, call_next):
# 1. Extract from subdomain
tenant = await self._resolve_from_subdomain(request)
# 2. Extract from header (service calls)
tenant_id = request.headers.get("X-Tenant-ID")
# 3. Set in request state
request.state.tenant_id = tenant_id or (tenant.id if tenant else None)
return await call_next(request)Query Filtering
All database queries MUST filter by tenant_id:
async def list_surveys(tenant_id: str):
query = select(Survey).where(Survey.tenant_id == tenant_id)
result = await db.execute(query)
return result.scalars().all()RBAC Helpers
Use common/rbac_helpers.py — never check is_super_admin from JWT payload:
from common.rbac_helpers import is_super_admin, is_host_admin
# Super admin has "super:admin" or "full:access" permission
if is_super_admin(user_permissions):
# Full system access
# Host admin has "host:manage_tenants" permission
if is_host_admin(user_permissions):
# Manage tenant-level settingsFrontend API Calls
Always use getApiClient() — never raw fetch():
import { getApiClient } from "@/lib/utils";
const apiClient = getApiClient(accessToken);
// Auto-handles auth + tenant headersTenant States
| State | is_active | is_dormant | is_trial | Description |
|---|---|---|---|---|
| Active Trial | true | false | true | Trial period |
| Dormant | true | true | false | Trial expired |
| Active Paid | true | false | false | Active subscription |
| Inactive | false | false | false | Host deactivated |
| Suspended | false | false | false | Policy violation |
