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'.
- Sets GUCs on all database aliases listed in
RLS_TENANTS["DATABASES"](default:["default"]). If setting GUCs on one alias fails, all previously-set aliases are cleaned up before the exception propagates. - Sets the internal
ContextVarstate for automatic query scoping (tenant users get auto-scoped queries; admin users and unauthenticated requests do not). - Sets
_rls_context_active=Trueto mark that an RLS context is active. This is used by strict mode to distinguish "middleware set context" from "no context at all". - Marks a
ContextVarflag that GUCs were set (used by the safety-net signal handler).
Response Phase (process_response)¶
- Resets the
_rls_context_activeflag (via saved token) to prevent cross-request leaks. - Clears the
ContextVarauto-scope state to prevent cross-request leaks. - If
USE_LOCAL_SETisFalse(default): clears both GUC variables on all configured database aliases 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
ContextVarGUC flag.
Exception Phase (process_exception)¶
If a view raises an unhandled exception, process_response may not run (depending
on middleware ordering). The process_exception handler ensures cleanup still happens:
- Resets the
_rls_context_activeflag (via saved token or fallback toFalse). - Resets the
ContextVarauto-scope state (via the saved token or fallback toNone). - Clears GUC variables (same logic as
process_response).
This prevents ContextVar leaks that could affect subsequent requests on the same
thread (WSGI) or async task (ASGI).
Error Handling¶
If setting a GUC fails during process_request (e.g., broken database connection):
- The
ContextVarstate is reset toNone. - 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
│ SET auto-scope state = None (no filter)
│ SET _rls_context_active = True
│
└── user.is_tenant_admin == False
└── SET rls.current_tenant = str(tenant_id)
SET rls.is_admin = 'false'
SET auto-scope state = tenant_id
SET _rls_context_active = True
│
▼
View executes (queries auto-scoped + filtered by RLS)
(strict mode: queries pass because _rls_context_active = True)
│
▼
RLSTenantMiddleware.process_response() (or process_exception on error)
│
├── RESET _rls_context_active (via saved token)
├── RESET auto-scope ContextVar (via saved token)
├── USE_LOCAL_SET == False → CLEAR both GUCs
└── USE_LOCAL_SET == True → no-op (transaction handles cleanup)
Multi-Database Support¶
By default, the middleware sets GUCs only on the default database connection.
In multi-database setups (read replicas, analytics databases), configure all
aliases that serve RLS-protected queries:
The middleware sets GUCs on all configured aliases during process_request and
clears them during process_response. A connection_created signal handler
also sets GUCs on lazily-created connections that don't exist when the middleware
runs (e.g., a replica connection opened by a database router mid-request).
See Configuration for details.
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.
Strict Mode¶
The middleware sets _rls_context_active=True for authenticated requests, which
satisfies the strict mode check. Unauthenticated requests do not set this
flag -- if strict mode is enabled, queries in unauthenticated views will raise
NoTenantContextError rather than silently returning zero rows.
This is the intended behavior: strict mode surfaces missing context at the point of query execution, making it easier to identify views that should require authentication.
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.