Scaling out your Django Multi-tenant App

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:

  1. Including package django-multitenant into your requirements.txt,
  2. Running 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:

  1. Inherit from TenantModel when creating your model
  2. Specify the field that is your tenant_id on each model.
  3. Set 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',
   )

Going beyond Django middleware

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.