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
       └── subdomain

Header (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)     # THIRD

Middleware 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 settings

Frontend API Calls

Always use getApiClient() — never raw fetch():

import { getApiClient } from "@/lib/utils";
const apiClient = getApiClient(accessToken);
// Auto-handles auth + tenant headers

Tenant States

Stateis_activeis_dormantis_trialDescription
Active TrialtruefalsetrueTrial period
DormanttruetruefalseTrial expired
Active PaidtruefalsefalseActive subscription
InactivefalsefalsefalseHost deactivated
SuspendedfalsefalsefalsePolicy violation