How to Enable Distributed Multi‑Tenant PostgreSQL (Citus) in a Python/Django App

This guide walks through migrating a single‑node Django multi‑tenant application to a distributed PostgreSQL backend using Citus, covering schema changes, primary‑key and unique‑constraint adjustments, integration of the django‑multitenant library, data distribution, range‑query updates, and middleware automation.

Hacker Afternoon Tea
Hacker Afternoon Tea
Hacker Afternoon Tea
How to Enable Distributed Multi‑Tenant PostgreSQL (Citus) in a Python/Django App

To scale a Django multi‑tenant app from a single database node to a distributed PostgreSQL (Citus) backend, several coordinated steps are required.

1. Introduce a tenant column to models

Add account_id to each model that belongs to an account, enabling fast queries for a specific tenant. For example, modify the Task model to include account = models.ForeignKey(Account, ...) and run python manage.py makemigrations.

For many‑to‑many relationships, add the tenant column to the through model and adjust queries to filter by account_id in the WHERE clause.

2. Include account_id in primary keys and unique constraints

Django creates a simple id primary key automatically; custom migrations are needed to replace it with a composite key (account_id, id). Example migration snippets use ALTER TABLE … DROP CONSTRAINT … and ADD CONSTRAINT … PRIMARY KEY (account_id, id). Unique constraints are similarly updated using unique=True or unique_together that reference account_id.

3. Update models to use TenantModelMixin and TenantForeignKey

Install django-multitenant (>=2.0.0, <3) and change the database engine in settings.py to 'django_multitenant.backends.postgresql'. Import the mixins and replace base classes with TenantModelMixin, defining tenant_id (e.g., 'id' for Account and 'account_id' for related models). Use TenantForeignKey for foreign keys between distributed tables.

Example model definitions:

from django.db import models
from django_multitenant.mixins import *
from django_multitenant.fields import TenantForeignKey

class Country(models.Model):
    name = models.CharField(max_length=255)

class Account(TenantModelMixin, models.Model):
    name = models.CharField(max_length=255)
    domain = models.CharField(max_length=255)
    subdomain = models.CharField(max_length=255)
    country = models.ForeignKey(Country, on_delete=models.SET_NULL)
    tenant_id = 'id'
    objects = TenantManager()

class Manager(TenantModelMixin, models.Model):
    name = models.CharField(max_length=255)
    account = models.ForeignKey(Account, related_name='managers', on_delete=models.CASCADE)
    tenant_id = 'account_id'
    objects = TenantManager()

class Project(TenantModelMixin, models.Model):
    account = models.ForeignKey(Account, related_name='projects', on_delete=models.CASCADE)
    managers = models.ManyToManyField(Manager, through='ProjectManager')
    tenant_id = 'account_id'
    objects = TenantManager()

class Task(TenantModelMixin, models.Model):
    name = models.CharField(max_length=255)
    project = TenantForeignKey(Project, on_delete=models.CASCADE, related_name='tasks')
    account = models.ForeignKey(Account, on_delete=models.CASCADE)
    tenant_id = 'account_id'
    objects = TenantManager()

class ProjectManager(TenantModelMixin, models.Model):
    project = TenantForeignKey(Project, on_delete=models.CASCADE)
    manager = TenantForeignKey(Manager, on_delete=models.CASCADE)
    account = models.ForeignKey(Account, on_delete=models.CASCADE)
    tenant_id = 'account_id'
    objects = TenantManager()

4. Distribute tables in Citus

Create an empty migration that calls tenant_migrations.Distribute for each table (e.g., Country, Account, Manager, Project, ProjectManager, Task) and apply it with python manage.py migrate. After this, the Django models work with the Citus backend.

5. Update the Django app to use range queries

The django-multitenant library automatically adds the appropriate WHERE account_id = … filter to all queries once set_current_tenant() is called. Example usage in a view:

# set the current tenant to the first account
s = Account.objects.first()
set_current_tenant(s)

# subsequent queries are scoped to that tenant
Project.objects.count()
Task.objects.filter(project__name='Very important project')

For convenience, a middleware can be added to set the tenant automatically from the logged‑in user:

# src/appname/middleware.py
from django_multitenant.utils import set_current_tenant

class MultitenantMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
    def __call__(self, request):
        if request.user and not request.user.is_anonymous:
            set_current_tenant(request.user.employee.company)
        response = self.get_response(request)
        return response

Register the middleware in settings/base.py by adding 'appname.middleware.MultitenantMiddleware' to the MIDDLEWARE list.

After completing these steps, the Django application is fully migrated to a distributed multi‑tenant PostgreSQL (Citus) setup, ready for data import and further view adjustments.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

PythonDjangoPostgreSQLdatabase-migrationCitusmultitenancydjango-multitenant
Hacker Afternoon Tea
Written by

Hacker Afternoon Tea

You might find something interesting here ^_^

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.