Middleware¶
RLSTenantMiddleware is the primary integration point for web applications. It
automatically sets the RLS context for each request based on the authenticated user.
Setup¶
Add the middleware to your MIDDLEWARE setting, after AuthenticationMiddleware:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
# Must come after AuthenticationMiddleware:
"django_rls_tenants.RLSTenantMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
Important
The middleware reads request.user, which is set by AuthenticationMiddleware.
Placing it before authentication will result in no tenant context being set.
How It Works¶
Request Phase (process_request)¶
- Checks if
request.userexists andis_authenticated. - If unauthenticated: does nothing. RLS policies block all access (fail-closed).
- If authenticated: reads the
TenantUserprotocol properties:is_tenant_admin: ifTrue, setsrls.is_admin = 'true'and clearsrls.current_tenant.rls_tenant_id: if not admin, setsrls.current_tenant = str(tenant_id)andrls.is_admin = 'false'.
- Marks a thread-local flag that GUCs were set (used by the safety-net signal handler).
Response Phase (process_response)¶
- If
USE_LOCAL_SETisFalse(default): clears both GUC variables to prevent cross-request leaks on persistent connections. - If
USE_LOCAL_SETisTrue: GUCs are automatically cleared at transaction end (by PostgreSQL), so explicit cleanup is skipped. - Clears the thread-local GUC flag.
Error Handling¶
If setting a GUC fails (e.g., broken database connection):
- Both GUCs are cleared on a best-effort basis.
- The exception is re-raised (Django returns a 500 response).
- This prevents partial GUC state from leaking to the next request.
Request Lifecycle Diagram¶
Request arrives
│
▼
AuthenticationMiddleware
│ sets request.user
▼
RLSTenantMiddleware.process_request()
│
├── user.is_authenticated == False → no-op (fail-closed)
│
├── user.is_tenant_admin == True
│ └── SET rls.is_admin = 'true'
│ CLEAR rls.current_tenant
│
└── user.is_tenant_admin == False
└── SET rls.current_tenant = str(tenant_id)
SET rls.is_admin = 'false'
│
▼
View executes (all queries filtered by RLS)
│
▼
RLSTenantMiddleware.process_response()
│
├── USE_LOCAL_SET == False → CLEAR both GUCs
└── USE_LOCAL_SET == True → no-op (transaction handles cleanup)
Safety Net¶
django-rls-tenants connects to Django's request_finished signal as a safety net.
If the middleware's process_response is somehow skipped (e.g., due to an unhandled
exception in another middleware), the signal handler clears the GUC variables.
This is a defense-in-depth measure -- the primary cleanup always happens in
process_response.
API-Agnostic Design¶
The middleware is API-agnostic. It works identically for:
- Django views and templates
- Django REST Framework
- GraphQL (Graphene, Strawberry, Ariadne)
- Async views (GUC setting uses sync database calls)
- Any other Django-compatible request handler
The only requirement is that request.user satisfies the TenantUser protocol.
Using Without Middleware¶
For non-web contexts (management commands, Celery tasks, scripts), use context managers instead of middleware:
from django_rls_tenants import tenant_context, admin_context
# In a management command:
with tenant_context(tenant_id=42):
orders = Order.objects.all()
# In a Celery task:
with admin_context():
all_users = User.objects.all()
See Context Managers for details.