Building a Distributed Multi‑Tenant Database with Django, Postgres, and Citus

This guide demonstrates how to build a distributed multi‑tenant database using Django with Postgres and Citus, covering installation, supported Django versions, model modifications via mixins or inheritance, middleware for tenant context, automatic composite foreign keys, and the library’s API with concrete code examples.

Hacker Afternoon Tea
Hacker Afternoon Tea
Hacker Afternoon Tea
Building a Distributed Multi‑Tenant Database with Django, Postgres, and Citus

Overview

Python/Django supports distributed multi‑tenant databases such as Postgres+Citus. By adding tenant context to queries, Citus can route queries to the correct node, enabling horizontal scaling.

Architecture

The library adopts the shared‑table design: each tenant has a tenant_id column in the shared tables. This assumes all tenant models/tables contain a tenant_id column.

Further reading on trade‑offs is linked (Citus blog).

Project source

https://github.com/citusdata/django-multitenant

Installation

pip install --no-cache-dir django_multitenant

Supported Django versions

Python 3.x – Django 2.2

Python 3.x – Django 3.2

Python 3.x – Django 4.0

Usage

Use Mixins or inherit from the custom model class.

Model changes

Import the library in any file:

from django_multitenant.fields import *
from django_multitenant.models import *

All models inherit TenantModel, e.g., class Product(TenantModel): Define a static variable tenant_id to specify the tenant column, e.g., tenant_id='store_id' Foreign keys in TenantModel subclasses must use TenantForeignKey instead of models.ForeignKey Example models:

class Store(TenantModel):
    tenant_id = 'id'
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=255)
    email = models.CharField(max_length=50)

class Product(TenantModel):
    store = models.ForeignKey(Store)
    tenant_id = 'store_id'
    name = models.CharField(max_length=255)
    description = models.TextField()
    class Meta(object):
        unique_together = ["id", "store"]

class Purchase(TenantModel):
    store = models.ForeignKey(Store)
    tenant_id = 'store_id'
    product_purchased = TenantForeignKey(Product)

Changing models with mixins

Import mixins: from django_multitenant.mixins import * Define models using TenantModelMixin together with models.Model, e.g., class Product(TenantModelMixin, models.Model): Define tenant_id static variable as before.

Use TenantForeignKey for foreign keys.

Example models with custom managers:

class ProductManager(TenantManagerMixin, models.Manager):
    pass

class Product(TenantModelMixin, models.Model):
    store = models.ForeignKey(Store)
    tenant_id = 'store_id'
    name = models.CharField(max_length=255)
    description = models.TextField()
    objects = ProductManager()
    class Meta(object):
        unique_together = ["id", "store"]

class PurchaseManager(TenantManagerMixin, models.Manager):
    pass

class Purchase(TenantModelMixin, models.Model):
    store = models.ForeignKey(Store)
    tenant_id = 'store_id'
    product_purchased = TenantForeignKey(Product)
    objects = PurchaseManager()

Automating composite foreign keys at the DB layer

Using TenantForeignKey automatically adds tenant_id to reference and join queries. To ensure composite foreign keys are created in the DB, change ENGINE in settings.py to django_multitenant.backends.postgresql:

'default': {
    'ENGINE': 'django_multitenant.backends.postgresql',
    ...
}

Setting the tenant

Write middleware that sets/unsets the tenant per request/session. Example:

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)
        return self.get_response(request)

Add the middleware path to MIDDLEWARE in settings.

In views that need tenant scoping, call set_current_tenant(t). Subsequent Django ORM calls will be automatically filtered by the current tenant.

Supported API

Model.objects.*

– most ORM operations respect tenant filtering. Model.save() – injects tenant_id for tenant‑inherited models.

Example usage:

s = Store.objects.all()[0]
set_current_tenant(s)
# Queries below automatically include tenant filters
Product.objects.get_queryset()
Purchase.objects.filter(id=1).filter(store__name='The Awesome Store').filter(product__description='All products are awesome')
Purchase.objects.filter(id=1).update(id=1)
p = Product(8, 1, 'Awesome Shoe', 'These shoes are awesome')
p.save()
Product.objects.count()
Product.objects.filter(store__name='The Awesome Store').count()
Product.objects.filter(name='Awesome Shoe')
Purchase.objects.filter(product__in=p)

Further reading

Citus introduction – turning Postgres into a distributed database

Distributed PostgreSQL – Citus architecture and concepts

Smartly.io scaling analysis service – sharding PostgreSQL with Citus

Official Citus multi‑tenant application example

Official Citus migration tutorial

Official Citus real‑time dashboard example

Official Citus time‑series data example

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.

Pythonbackend developmentDjangoMulti‑TenantPostgreSQLCitus
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.