Configuration¶
All configuration is provided through the RLS_TENANTS dictionary in your Django
settings module.
Settings Reference¶
RLS_TENANTS = {
"TENANT_MODEL": "myapp.Tenant",
"DATABASES": ["default"],
"STRICT_MODE": False,
"TENANT_FK_FIELD": "tenant",
"GUC_PREFIX": "rls",
"USER_PARAM_NAME": "as_user",
"TENANT_PK_TYPE": "int",
"USE_LOCAL_SET": False,
}
TENANT_MODEL¶
| Type | str |
| Default | None (required) |
| Example | "myapp.Tenant" |
Dotted path to your tenant model, in the same format as Django's AUTH_USER_MODEL.
This model is used as the target for the auto-generated ForeignKey on RLSProtectedModel
subclasses.
DATABASES¶
| Type | list[str] |
| Default | ["default"] |
Database aliases to set GUC variables on. In multi-database setups (e.g., read replicas), add all aliases that serve RLS-protected queries.
The middleware sets GUCs on all configured aliases during each request (with
USE_LOCAL_SET=True, on its synchronous path only). With session GUCs, a
connection_created signal handler also sets GUCs on connections created lazily,
after the middleware ran, on the thread that set the request's GUCs. Changed in 1.4.0:
the GUC affinity guard applies the active
context on every configured alias before each statement, on any thread, whichever alias
the context was entered on, in both GUC modes (see
Running under ASGI). A connection checked
out of Django's connection pool (OPTIONS["pool"]) is cleared of every RLS GUC first,
whether or not a request is active (see
Django's Connection Pool).
Warning
Each alias must exist in Django's DATABASES setting. A typo (e.g., "replca")
will trigger system check W006 at startup and cause runtime errors.
STRICT_MODE¶
| Type | bool |
| Default | False |
When True, TenantQuerySet evaluation methods raise NoTenantContextError
if no RLS context is active. This catches accidental queries outside a
tenant_context(), admin_context(), for_user(), or RLSTenantMiddleware
scope at the point of query execution rather than silently returning zero rows.
Guarded methods: _fetch_all() (iteration), count(), exists(),
aggregate(), update(), delete(), iterator(), aiterator(), bulk_create(),
bulk_update(), get(), first(), last(). A queryset whose results are already
cached answers iteration, count() and exists() without a query and is not checked,
so results fetched inside a context (including prefetch_related() caches) can be read
after it ends.
RLS_TENANTS = {
"TENANT_MODEL": "myapp.Tenant",
"STRICT_MODE": True, # Recommended for development
}
Recommended usage: Enable in development and staging to surface missing context early. In production, the fail-closed behavior (zero rows) is still the safety net, but strict mode makes the failure loud instead of silent.
import os
RLS_TENANTS = {
"TENANT_MODEL": "myapp.Tenant",
"STRICT_MODE": os.environ.get("DJANGO_ENV") != "production",
}
Note
Strict mode does not change the database-level behavior. RLS policies continue to enforce fail-closed isolation regardless of this setting. Strict mode adds an application-level check that raises before the query reaches the database.
Actionable error hints
The NoTenantContextError raised by strict mode -- and by tenant_context(),
admin_context(), and @with_rls_context -- carries a Hint: line telling you
how to establish a context. The bare message and the hint are also available
separately via the .message and .hint attributes:
from django_rls_tenants.exceptions import NoTenantContextError
try:
list(Order.objects.all()) # STRICT_MODE=True, no active context
except NoTenantContextError as exc:
log.error(exc.message) # "RLS strict mode: query on Order attempted ..."
show_hint(exc.hint) # "Establish an RLS context before the query: ..."
TENANT_FK_FIELD¶
| Type | str |
| Default | "tenant" |
The name of the ForeignKey field added to RLSProtectedModel subclasses. The database
column will be {field_name}_id (e.g., tenant_id).
RLS_TENANTS = {
"TENANT_MODEL": "myapp.Tenant",
"TENANT_FK_FIELD": "organization", # field: organization, column: organization_id
}
GUC_PREFIX¶
| Type | str |
| Default | "rls" |
Prefix for PostgreSQL GUC (Grand Unified Configuration) variable names. The library derives two GUC variables from this prefix:
{prefix}.current_tenant-- holds the current tenant ID{prefix}.is_admin--"true"for admin bypass
# Default: rls.current_tenant, rls.is_admin
RLS_TENANTS = {
"GUC_PREFIX": "rls",
}
# Custom: myapp.current_tenant, myapp.is_admin
RLS_TENANTS = {
"GUC_PREFIX": "myapp",
}
Warning
If you change GUC_PREFIX after running migrations, the RLS policies in the database
will still reference the old GUC names. You will need to recreate the migrations
or manually update the policies.
USER_PARAM_NAME¶
| Type | str |
| Default | "as_user" |
The parameter name that the @with_rls_context decorator looks for in decorated
function signatures. When the decorator finds this parameter, it extracts the user
object and sets the appropriate RLS context.
@with_rls_context
def process_order(request, current_user): # matches USER_PARAM_NAME
orders = Order.objects.all() # automatically scoped
See Context Managers for details on @with_rls_context.
TENANT_PK_TYPE¶
| Type | str |
| Default | "int" |
| Allowed values | "int", "bigint", "uuid" |
The SQL type used to cast the tenant ID in the RLS policy. Must match the primary key type of your tenant model.
# For UUID primary keys:
RLS_TENANTS = {
"TENANT_MODEL": "myapp.Tenant",
"TENANT_PK_TYPE": "uuid",
}
USE_LOCAL_SET¶
| Type | bool |
| Default | False |
When True, uses SET LOCAL (transaction-scoped) instead of set_config()
(session-scoped) to set GUC variables.
When to enable:
- You are using a connection pooler (PgBouncer, pgpool) in transaction mode.
SET LOCALensures GUCs are automatically cleared at transaction end, preventing cross-request leaks through pooled connections.
What changes when enabled:
- On the synchronous path (WSGI, or ASGI with a synchronous-only middleware listed after
it),
RLSTenantMiddlewareopenstransaction.atomic()on every configured alias around each authenticated request, so theSET LOCALGUCs cover the whole request and are cleared at commit/rollback.ATOMIC_REQUESTSis not required (it nests as a savepoint if you keep it). - On the middleware's async path (1.4.0+; ASGI, when every middleware listed after it can
run asynchronously), it opens no transaction and writes no GUC. The
GUC affinity guard sets the request's
GUCs for each statement, in a transaction of its own when none is open, or once per
transaction for a sync view under
ATOMIC_REQUESTS. So a WSGI request is atomic in this mode and a request on the async path is not. Before 1.4.0 the middleware raisedRLSConfigurationErrorunder ASGI. - Outside a request (Celery tasks, management commands, scripts), the context managers and
for_user()no longer needtransaction.atomic()(1.4.0+): the guard gives each statement the context in a transaction of its own, which costs three extra round trips per statement in autocommit. Wrapping a batch of statements intransaction.atomic()is still worth it for performance (see Code Outside a Request Transaction for the cost by placement). This holds for aliases inRLS_TENANTS["DATABASES"]; on any other alias a context manager outsideatomic()raises (RuntimeErrorfrom the sync managers,RLSConfigurationErrorfrom the async ones).for_user()'supdate_or_create(),bulk_create(),bulk_update()anddelete()still open a transaction of their own when none is open. - When a context ends, and on any connection after a statement run under a context's values, the values a session holds for itself count as unknown. The next statement run outside any context, in autocommit or in a transaction, clears them first for the session (one extra statement), in case code inside the context wrote the GUCs for the session.
- On the synchronous path, middleware implementing
process_exception, and function-based middleware factories, must precedeRLSTenantMiddleware; unsafe ordering raisesRLSConfigurationErrorwhen the middleware is loaded. See middleware ordering.
See Connection Pooling for detailed setup instructions, and Async Deployment for this mode under ASGI.
System Checks¶
django-rls-tenants registers Django system checks that warn about common misconfigurations:
| Check | Severity | Description |
|---|---|---|
W001 |
Warning | RLSConstraint.guc_tenant_var doesn't match GUC_PREFIX-derived name |
W002 |
Warning | RLSConstraint.guc_admin_var doesn't match GUC_PREFIX-derived name |
W003 |
— | Retired in 1.3.1 (duplicate of W007) |
W004 |
Warning | CONN_MAX_AGE > 0 with USE_LOCAL_SET=False (session GUCs may leak) |
W005 |
Warning | Default database connection role is a superuser or has BYPASSRLS (RLS bypassed) |
W006 |
Warning | DATABASES contains an alias not defined in settings.DATABASES |
W007 |
— | Retired in 1.3.2; the middleware opens its own transaction, so ATOMIC_REQUESTS is no longer required for USE_LOCAL_SET |
W008 |
Warning | RLS_TENANTS['TENANT_MODEL'] does not resolve to an installed model |
W009 |
Warning | Tenant field referenced by an RLSProtectedModel's RLS policy is missing |
W010 |
Warning | Django's connection pool (OPTIONS["pool"]) with USE_LOCAL_SET=False and no pool reset callback |
W011 |
Warning | PostgreSQL applies a database- or role-level default (ALTER DATABASE/ROLE ... SET) for a GUC the RLS policies read, in force in every new session |
Run python manage.py check to see any warnings.
Typo Detection¶
If you add an unrecognized key to RLS_TENANTS, the library emits a UserWarning
listing the known keys. This helps catch typos like TENANT_FK_NAME (should be
TENANT_FK_FIELD).
Environment Variables (Test Suite)¶
The test suite reads database configuration from environment variables:
| Variable | Default |
|---|---|
POSTGRES_DB |
django_rls_tenants_test |
POSTGRES_USER |
postgres |
POSTGRES_PASSWORD |
postgres |
POSTGRES_HOST |
localhost |
POSTGRES_PORT |
5432 |