How Django RLS Works: Architecture and Flow

Understanding how Django RLS works under the hood can help you make better decisions when implementing Row Level Security in your applications. Let’s explore the architecture and execution flow.

Architecture Overview

Django RLS sits between your Django application and PostgreSQL, managing the RLS policies and context:

Django Application
        ↓
   Django RLS
      ↙    ↘
Models   Middleware → PostgreSQL (with RLS policies)
     ↖    ↗
   Context Management

The Four-Step Process

1. Define Models with RLS Policies

You start by defining your models with RLS policies:

from django_rls.models import RLSModel
from django_rls.policies import UserPolicy

class Document(RLSModel):
    title = models.CharField(max_length=200)
    content = models.TextField()
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    
    class Meta:
        rls_policies = [
            UserPolicy('owner_policy', user_field='owner')
        ]

When you run migrations, Django RLS automatically creates PostgreSQL RLS policies based on your configuration.

2. Set Context via Middleware

Django RLS includes middleware that automatically sets the user context for each request:

MIDDLEWARE = [
    'django_rls.middleware.RLSMiddleware',
]

This middleware:

  • Extracts the current user from the request
  • Sets PostgreSQL context variables (like current_user_id)
  • Makes context available throughout the request lifecycle

3. Execute Queries with Automatic Filtering

When you execute a query, PostgreSQL applies the RLS policies:

# Your code (same for all users)
my_docs = Document.objects.all()

# Behind the scenes:
# User A sees only their documents
# User B sees only their documents
# PostgreSQL enforces this at the database level

PostgreSQL’s RLS engine intercepts the query and applies the policies automatically.

4. Return Filtered Results

Only rows that pass the RLS policy are returned:

Query: SELECT * FROM documents
       ↓
RLS Policy Check: WHERE owner_id = current_user_id()
       ↓
Filtered Results: Only user's documents returned

How Policies Are Evaluated

When you define a policy, Django RLS translates it to a PostgreSQL RLS policy:

# Django RLS definition
UserPolicy('owner_policy', user_field='owner')

# Creates PostgreSQL policy like:
CREATE POLICY owner_policy ON documents
    USING (owner_id = current_user_id())
    WITH CHECK (owner_id = current_user_id());

Context Management

Context is managed through PostgreSQL’s session settings:

# Django RLS Middleware does this automatically:
from django.db import connection

with connection.cursor() as cursor:
    cursor.execute(
        "SELECT set_config('app.user_id', %s, false)",
        [str(request.user.id)]
    )

This sets a context variable that RLS policies can reference.

Multi-Tenant Example

Let’s trace through a multi-tenant scenario:

class Document(RLSModel):
    tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
    
    class Meta:
        rls_policies = [
            TenantPolicy('tenant_policy', tenant_field='tenant'),
            UserPolicy('owner_policy', user_field='owner'),
        ]

Execution Flow:

  1. Request comes in from User A in Tenant X
  2. Middleware sets context: app.tenant_id = X, app.user_id = A
  3. Query executed: Document.objects.all()
  4. Tenant policy applied: WHERE tenant_id = X
  5. Owner policy applied: WHERE owner_id = A
  6. Results returned: Only documents in Tenant X owned by User A

Performance Considerations

Query Optimization

PostgreSQL optimizes queries knowing about RLS policies:

# This query:
Document.objects.filter(status='active').count()

# Is executed as:
SELECT COUNT(*) FROM documents
WHERE status = 'active'
  AND owner_id = current_user_id()  -- Added by RLS

PostgreSQL combines your filters with RLS conditions for optimal performance.

Index Usage

RLS policies work seamlessly with indexes:

-- Your index
CREATE INDEX idx_documents_status ON documents(status);

-- RLS filters efficiently use your indexes
-- Even though the policy adds conditions

Migration and Policy Management

When you change your models, Django RLS handles policy updates through migrations:

$ python manage.py makemigrations
$ python manage.py migrate

Django RLS automatically:

  • Creates new policies when you add them
  • Updates policies when you modify them
  • Removes policies when you delete them

Testing

Django RLS provides utilities to test policies:

from django_rls.test_utils import RLSTestCase

class DocumentTestCase(RLSTestCase):
    def test_user_isolation(self):
        # Sets up user context automatically
        doc = Document.objects.create(owner=self.user)
        self.assertEqual(Document.objects.count(), 1)  # Only their doc

Summary

Django RLS works by:

  1. Defining policies in your models
  2. Creating PostgreSQL RLS policies through migrations
  3. Setting user context via middleware for each request
  4. Filtering queries automatically at the database level

This approach keeps your application code clean while ensuring security at the database level, where it matters most.