POSETTE: An Event for Postgres 2025 will happen virtually Jun 10-12, 2025. Call for Speakers is open! 💥
POSETTE: An Event for Postgres 2025 will happen virtually Jun 10-12, 2025. Call for Speakers is open! 💥
Written by Sai Srirampur
November 14, 2017
There are a number of data architectures you could use when building a multi-tenant app. Some, such as using one database per customer or one schema per customer, have trade-offs when it comes to larger scale. The other option is to build the notion of tenancy directly into the logic of your SaaS application. With django-multitenant
and Citus, built-in tenancy becomes much easier to put in place for your application without having to re-invent the wheel yourself.
Our django-multitenant Python library, enables easy scale out of applications that are built on top of Django and follow a multi tenant data model. This Python library has evolved from our experience working with SaaS customers, scaling out their multi-tenant apps.
You can get started at the application level setup with:
django-multitenant
into your requirements.txt
, pip install -r requirements.txt
, Once you've done the installation. You'll want to import that package and begin updating your models to leverage the package:
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"]
In the above there are three changes to your app which you'd make to each model that relates to a tenant:
TenantModel
when creating your model tenant_id
on each model.id
and store
as a unique_together
constraint. Do the above and you should be set to prep your multi-tenant application for scaling when you need it. But there's more.
What happens when you don't want to have to think about scoping the tenant? With django-multitenant
you can easily put in place your own logic that hooks into the Django authentication infrastructure and sets the appropriate tenant as a request comes in.
To do this we'll write some middleware which sets/unsets a tenant for each session/request. This way you don't have to worry about setting a tenant on a per view basis. Let’s set it at authentication and the library would ensure the rest (adding tenant_id filters to the queries). This way you do not need to worry about setting a tenant on a per view basis.
Here's a sample of what such would look like:
class SetCurrentTenantFromUser(object):
def process_request(self, request):
if not hasattr(self, 'authenticator'):
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
self.authenticator = JSONWebTokenAuthentication()
try:
user, _ = self.authenticator.authenticate(request)
except:
# TODO: handle failure
return
try:
# Assuming your app has a function to get the tenant associated for a user
current_tenant = get_tenant_for_user(user)
except:
# TODO: handle failure
return
set_current_tenant(current_tenant)
def process_response(self, request, response):
set_current_tenant(None)
return response
MIDDLEWARE_CLASSES = (
'our_app.utils.multitenancy.SetCurrentTenantFromUser',
)
Middleware works great and would suffice to scope your application, another approach is to set the tenant using setcurrenttenant(t) api in all the views which you want to be scoped based on tenant. This allows you to scope django API calls automatically (without specifying explicit filters) to a single tenant. If the current_tenant is not set, then the default/native API without tenant scoping is used.
Let’s take a look at how that works:
def application_function:
# current_tenant can be stored as a SESSION variable when a user logs in.
# This should be done by the app
t = current_tenant
#set the tenant
set_current_tenant(t);
#Django ORM API calls;
#Command 1;
#Command 2;
Most of API calls are supported by this library, some of the examples are listed below
# Most of the APIs under Model.objects.* except select_related().
# Examples:
s=Store.objects.all()[0]
set_current_tenant(s)
# All the below API calls would add suitable tenant filters.
# Simple get_queryset()
Product.objects.get_queryset()
# Joins
Purchase.objects.filter(id=1).filter(store__name='The Awesome Store').filter(product__description='All products are awesome')
# Updates
Purchase.objects.filter(id=1).update(id=1)
# Saves
p=Product(8,1,'Awesome Shoe','These shoes are awesome')
p.save()
# Aggregates
Product.objects.count()
Product.objects.filter(store__name='The Awesome Store').count()
# Subqueries
Product.objects.filter(name='Awesome Shoe');
Purchase.objects.filter(product__in=p);
You can find the django-multitenant library from Citus on GitHub. If you need any help in getting your multi-tenant application prepared to scale out feel free to join us in our Slack channel and we’d be happy to help where needed.