Skip to content

Configuration

All configuration is provided through the RLS_TENANTS dictionary in your Django settings module.

Settings Reference

settings.py
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.

RLS_TENANTS = {
    "TENANT_MODEL": "organizations.Organization",
}

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.

RLS_TENANTS = {
    "TENANT_MODEL": "myapp.Tenant",
    "DATABASES": ["default", "replica"],
}

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.

settings.py
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.

RLS_TENANTS = {
    "USER_PARAM_NAME": "current_user",
}
@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 LOCAL ensures 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), RLSTenantMiddleware opens transaction.atomic() on every configured alias around each authenticated request, so the SET LOCAL GUCs cover the whole request and are cleared at commit/rollback. ATOMIC_REQUESTS is 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 raised RLSConfigurationError under ASGI.
  • Outside a request (Celery tasks, management commands, scripts), the context managers and for_user() no longer need transaction.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 in transaction.atomic() is still worth it for performance (see Code Outside a Request Transaction for the cost by placement). This holds for aliases in RLS_TENANTS["DATABASES"]; on any other alias a context manager outside atomic() raises (RuntimeError from the sync managers, RLSConfigurationError from the async ones). for_user()'s update_or_create(), bulk_create(), bulk_update() and delete() 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 precede RLSTenantMiddleware; unsafe ordering raises RLSConfigurationError when the middleware is loaded. See middleware ordering.
RLS_TENANTS = {
    "TENANT_MODEL": "myapp.Tenant",
    "USE_LOCAL_SET": True,
}

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