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.
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_multitenantSupported 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
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
