🎉 django-rls: Declarative Row-Level Security Policies for Django + Postgres

Hi everyone,

I wanted to share a project I’ve been working on to make Postgres Row-Level Security (RLS) easier and more maintainable in Django projects.

Background
Postgres RLS is a powerful feature for isolating tenant or user data at the database level, but integrating it smoothly with Django isn’t always straightforward:

  • You often end up scattering raw SQL across migrations and setup scripts.
  • It’s tricky to keep policies and models in sync over time.
  • The logic can become opaque for teams who haven’t worked with RLS before.

What is django-rls?
django-rls is an open-source package that lets you:

:white_check_mark: Define RLS policies declaratively right alongside your Django models
:white_check_mark: Auto-generate migrations that create and update policies in Postgres
:white_check_mark: Attach policies dynamically to different database roles (e.g., tenant users, admins)
:white_check_mark: Keep your tenant isolation logic explicit, version-controlled, and testable

Quick Example

Here’s a minimal example of defining a policy on a model:

from django_rls.models import RLSModel, RLSPolicy

class Invoice(RLSModel):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    
    class RLS:
        policies = [
            RLSPolicy(
                name="tenant_isolation",
                using="customer_id = current_setting('myapp.tenant_id')::integer"
            )
        ]

When you run makemigrations, django-rls will generate the SQL to create or update this policy in Postgres automatically.

Why you might care
If you’re building SaaS applications with multi-tenancy or any scenario where you want to lock down access at the DB layer, RLS can be a strong line of defense. The goal of django-rls is to make this feel as natural as defining Meta options on a model.

Project & Links

Looking for Feedback
I’d love to hear:

  • If you’ve tried RLS before, what challenges you faced?
  • Any features or integrations you’d find valuable?
  • Suggestions for improvements or edge cases I might have missed.

Thanks for reading—and I hope this helps make RLS more approachable in Django projects. Happy to answer any questions!

Looks like a nice package!

Have you considered hooking into Django’s existing permission framework, using RLS to enforce permissions? That would pretty cool!

PS Do check your original message you left some of the AI response that wasn’t intended for the message itself.

1 Like

Yes, I am thinking about it, the only caveat is, I don’t want to do this in the application layer, as it should make the application slow, and would defeat the sole purpose of RLS, so having being able to declare the similar permissions as policies on RLS, and then not add them on viewsets but model.

But thanks for the suggestions and the AI leftovers, have updated it.

2 Likes

Thanks so much for the kind words, @nanorepublica!

Integrating with Django’s permission framework is a fantastic idea and definitely something I’ll look into for the roadmap.

Actually, one of the key features I really wanted to highlight with this release is “Pythonic Policies”. Instead of writing raw SQL strings for RLS policies, you can now define them using standard Django Q objects! django-rls handles the heavy lifting of converting these Q filters into the correct SQL, including handling related lookups by rewriting them into subqueries (since Postgres RLS policies don’t support direct JOINs).

Here is a quick example

from django.db import models
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 = [
            # Pythonic Policy: Owner OR Public
            ModelPolicy(
                'access_policy',
                filters=Q(owner=RLS.user_id()) | Q(is_public=True)
            ),
        ]

This makes defining security rules feel much more native to Django developers.

3 Likes