Started as a scratch-your-own-itch weekend library, now the subject of a hands-on workshop at DjangoCon Europe. PostgreSQL row-level security expressed as Django model metadata: policies live next to the models they protect and are enforced by the database, not the ORM.
How it works
Models inherit from RLSModel and declare policies in Meta.rls_policies.
Tenant- and user-scoped policies come ready-made (TenantPolicy,
UserPolicy), and ModelPolicy accepts plain Django Q objects — which
compile down to real PostgreSQL CREATE POLICY statements:
from django.db.models import Q
from django_rls.models import RLSModel
from django_rls.policies import ModelPolicy, RLS
class Project(RLSModel):
name = models.CharField(max_length=100)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
is_public = models.BooleanField(default=False)
class Meta:
rls_policies = [
ModelPolicy("access_policy",
filters=Q(owner=RLS.user_id()) | Q(is_public=True)),
]
RLSContextMiddleware sets the session context per request, context
processors can inject anything dynamic (user IP, session data), and custom
migration operations plus enable_rls/disable_rls management commands
handle the DDL side.
Under the hood
- Context reads are cached per statement:
current_setting()is wrapped in a scalar subquery so Postgres evaluates it once as an InitPlan, not per row. - Joined lookups like
company__namecompile to subqueries inside the policy. - Field names are validated before DDL generation, keeping injection out of generated policies.
- Hierarchical tenancy (nested organizations) works via recursive CTEs, and custom user models — UUID primary keys included — are supported.
Runs on Python 3.10–3.13 against PostgreSQL 12+. Published on PyPI as
django-rls (BSD-3-Clause), with docs at
django-rls.com.