Backend Multi-Tenancy

Backend Multi-Tenancy

Tenancy is implemented with a thread-local tenant context plus tenant-aware model managers.

Tenant Context (Thread-Local)

Source of truth: backend/apps/tenants/utils.py.

  • set_current_tenant_id(tenant_id) stores the tenant in thread-local storage.
  • get_current_tenant_id() reads it back.

Request Middleware

Source of truth: backend/apps/tenants/middleware.py.

  • TenantMiddleware runs for authenticated users and calls request.user.set_session_tenant(request=request).
  • TenantRequiredMiddleware enforces that authenticated API requests have an active tenant:
    • allows /_allauth/ without tenant
    • allows /api/v1/workspaces without tenant
    • for other /api/... routes: returns 409 {"code":"TENANT_REQUIRED"} if no active tenant

User Tenant Selection Rules

Tenant selection is implemented on the user model via set_session_tenant():

  • Source: backend/apps/users/mixins.py
  • Priority: explicit tenant_id -> session tenant_id -> thread-local -> DB fallback

Tenant-Aware Models And Managers

Tenant-aware managers filter by the current thread-local tenant id:

  • TenantModelManager and TenantModelIDMixin: backend/apps/tenants/models/mixins.py
  • Pattern:
    • .objects is tenant-scoped (returns none if tenant context is missing)
    • .unscoped bypasses filtering and should be used sparingly

Practical implications:

  • If you see "empty" query results unexpectedly, confirm tenant context is set (middleware or task setup).
  • Use .unscoped only for admin/cross-tenant operations, and always add an explicit tenant_id=... filter in those cases.

Celery Tasks And Tenancy

Tasks must accept tenant_id and set tenant context before querying.

Helper for enqueueing:

  • backend/apps/common/utils/core.py (run_async_task())

Last updated June 21, 2026