Hi everyone,
I’d like to share a library I’ve been working on: django-rls-tenants.
It moves tenant data isolation from “something your Django code must remember to do” to “something PostgreSQL enforces whether you remember or not,” using Row-Level Security policies.
How I got here
I was building a multi-tenant Django app and evaluating the usual options. Schema-per-tenant felt heavy for what I needed (and the migration-time scaling worried me). Application-level filtering with custom managers was cleaner, but I kept coming back to one question: what happens when someone writes a management command, a Celery task, or a raw SQL query and forgets the tenant filter?
The answer – “all tenant data gets returned” – didn’t sit well with me. So I started looking at PostgreSQL’s Row-Level Security, which has been a core feature since PG 9.5. RLS lets you attach policies to tables that the database enforces on every query – ORM, raw SQL, dbshell, everything. If no tenant context is set, you get zero rows back, not all rows. Fail-closed by default.
What the library does
-
Inherit from
RLSProtectedModel– the tenant FK and RLS policy are created automatically duringmigrate -
RLSTenantMiddlewaresets the PostgreSQL session variable per request (afterAuthenticationMiddleware) -
tenant_context()andadmin_context()context managers for Celery tasks, management commands, scripts -
Defense in depth:
for_user()applies both an ORM.filter()and the database-level GUC -
Test utilities (
assert_rls_enabled(),rls_as_tenant(), …) and acheck_rlsmanagement command for CI
Supports Python 3.11–3.14, Django 4.2–6.0, PostgreSQL 15+. MIT licensed.
Try it
The fastest way to see it working is the included example app – clone the repo, run docker compose up in the example/ directory, and you get a Django app with 3 tenants and sample data. Log in as different users and watch how Note.objects.all() – with no tenant filter in the view – returns only that user’s tenant data. Full docs, quickstart, and the example are all linked from the GitHub repo.
I wrote a longer blog post explaining the approach, the tradeoffs, and when RLS is (and isn’t) the right fit – happy to share if anyone’s interested.
I’d genuinely appreciate feedback on the approach, the API design, or use cases I haven’t considered.