Skip to content

django-rls-tenants

Database-enforced multitenancy for Django using PostgreSQL Row-Level Security.

CI PyPI version Python versions Django versions Code style: Ruff Type checked: mypy License: MIT


Why Row-Level Security?

Most Django multitenancy libraries filter data in application code -- through ORM managers, middleware, or custom querysets. If a developer forgets a filter, or writes raw SQL, tenant data leaks silently.

django-rls-tenants pushes isolation into the database using PostgreSQL Row-Level Security policies. Every query -- ORM, raw SQL, dbshell, migration scripts -- is subject to the same policy, provided the application connects as a role without SUPERUSER or BYPASSRLS. The database itself becomes the trust boundary for forgotten filters and unscoped queries; see the Security Model for what that does and does not cover.

Key Features

  • Database-enforced isolation -- RLS policies apply to every query, not just ORM calls.
  • Automatic query scoping -- queries are filtered at both ORM and database levels for defense-in-depth.
  • Fail-closed by default -- missing tenant context returns zero rows, never leaks data.
  • Optional strict mode -- STRICT_MODE=True raises NoTenantContextError on unscoped queries, turning silent empty results into loud errors during development.
  • Single schema, single database -- no schema-per-tenant overhead.
  • API-agnostic -- works with Django REST Framework, GraphQL, async views, management commands.
  • M2M isolation -- auto-detected subquery-based RLS policies on M2M through tables.
  • Tenant-aware Django admin -- RLSTenantModelAdmin scopes changelists, forms, and related dropdowns to the current tenant, with a tenant switcher for cross-tenant admins. See the Admin guide.
  • Native Celery integration -- @rls_task carries the active tenant context from enqueue to the worker, across chains, groups, and chords. See the Celery Tasks guide.
  • Raw SQL helpers -- safe_tenant_sql() builds a tenant-filter predicate; current_tenant_value_sql() returns the current tenant ID expression for inserts and projections. Both reuse the expressions used by RLS policies. See the Raw SQL guide.
  • Clean internal layering -- generic rls/ primitives separate from tenants/ conveniences.
  • Configurable escape hatches -- bypass flags for authentication, admin, migrations.
  • Drop-in for new projects -- abstract model, middleware, and test helpers included.

I want to...

Goal Start here
Install and protect my first model Quick Start, then Protecting Models
Expose the tenant on my User model User Integration
Set the tenant context for every request Middleware
Set the context in a script or management command Context Managers
Run a Celery task under the right tenant Celery Tasks
Use the tenant context in an async view or consumer Async Code
Use the Django admin across tenants Admin
Write a test that proves isolation fails closed Testing
Test async code under a tenant context Async Tests
Write raw SQL that stays tenant-scoped Raw SQL
Let authentication or migrations bypass RLS Bypass Mode
Run behind PgBouncer, with Django's connection pool or with persistent connections Connection Pooling
Deploy on an ASGI server (Uvicorn, Daphne, Hypercorn) Async Deployment
Index tables so RLS stays fast Performance & Indexing
Verify the policies are really in the database Management Commands
Migrate an existing app or another multitenancy library Migration Guide
Understand what RLS does and does not guarantee Security Model
Fix something that is wrong right now Troubleshooting

How It Works

┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│   Request    │───▶│  Middleware  │───▶│  Set GUCs    │
│              │    │  resolves    │    │  on PG conn  │
│              │    │  tenant user │    │              │
└──────────────┘    └──────────────┘    └──────┬───────┘
                                               │
                                               ▼
                                       ┌──────────────┐
                                       │  PostgreSQL  │
                                       │  RLS Policy  │
                                       │  filters rows│
                                       └──────────────┘
  1. Middleware reads request.user and extracts tenant identity via the TenantUser protocol.
  2. GUC variables (rls.current_tenant, rls.is_admin) are set on the PostgreSQL connection.
  3. Auto-scoping adds WHERE tenant_id = X at the ORM level, enabling composite index usage (see the Performance & Indexing guide).
  4. RLS policies (created automatically via Django migrations) filter every query at the database level as a safety net.
  5. Fail-closed: if no GUC is set, the policy returns zero rows -- no data leak is possible.

Comparison with Alternatives

Feature django-rls-tenants django-tenants django-multitenant
Isolation mechanism RLS policies Separate schemas ORM rewriting
Raw SQL protected Yes Yes (schemas) No
Single schema Yes No Yes
No connection routing Yes No Depends
Fail-closed on missing ctx Yes N/A No
Works with any API layer Yes Yes Yes

Requirements

Dependency Version
Python 3.12, 3.13, 3.14
Django 5.2, 6.0, 6.1
PostgreSQL 15, 16, 17, 18

See the supported-versions policy for how these lists change.

Quick Start

pip install django-rls-tenants
# settings.py
INSTALLED_APPS = [
    # ...
    "django_rls_tenants",
]

RLS_TENANTS = {
    "TENANT_MODEL": "myapp.Tenant",
}

MIDDLEWARE = [
    # ...
    "django_rls_tenants.RLSTenantMiddleware",
]

See the full Quick Start tutorial for a complete walkthrough.

Try the demo

See django-rls-tenants in action with the example project. cd example && docker compose up, and explore tenant isolation in under 5 minutes.

License

MIT