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.
TenantMiddlewareruns for authenticated users and callsrequest.user.set_session_tenant(request=request).TenantRequiredMiddlewareenforces that authenticated API requests have an active tenant:- allows
/_allauth/without tenant - allows
/api/v1/workspaceswithout tenant - for other
/api/...routes: returns409 {"code":"TENANT_REQUIRED"}if no active tenant
- allows
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-> sessiontenant_id-> thread-local -> DB fallback
Tenant-Aware Models And Managers
Tenant-aware managers filter by the current thread-local tenant id:
TenantModelManagerandTenantModelIDMixin:backend/apps/tenants/models/mixins.py- Pattern:
.objectsis tenant-scoped (returns none if tenant context is missing).unscopedbypasses 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
.unscopedonly for admin/cross-tenant operations, and always add an explicittenant_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