django-rls-tenants¶
Database-enforced multitenancy for Django using PostgreSQL Row-Level Security.
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=TrueraisesNoTenantContextErroron 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 --
RLSTenantModelAdminscopes 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_taskcarries 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 fromtenants/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│
└──────────────┘
- Middleware reads
request.userand extracts tenant identity via theTenantUserprotocol. - GUC variables (
rls.current_tenant,rls.is_admin) are set on the PostgreSQL connection. - Auto-scoping adds
WHERE tenant_id = Xat the ORM level, enabling composite index usage (see the Performance & Indexing guide). - RLS policies (created automatically via Django migrations) filter every query at the database level as a safety net.
- 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¶
# 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.