Django Multitenant Setup Article Cover
Home Django Django Multitenant Applications

Django Multitenant Applications

04 October, 2024

Let's discover in this tutorial a simple way of setting up Django to allow multitenancy. Have you noticed how some webapps provide dedicated subdomains for each of their customers? For example a SaaS product might offer to their customers something like: the-awesome-org.saasproduct.com. Multitenancy means offering the same web application to different customers while keeping data isolated.

A social network is not a multitenant application because it doesn't provide data isolation. The users on the social network share the same data.

Frequently Asked Questions

In the row-level multitenancy model, the tenants share everything. The separation needs to be done in the application code. You need to make the application tenant-aware and always make sure the queries don't expose data they shouldn't.

Authentication works pretty much the same in principle. The main difference is that now users will belong to a Tenant, so when authenticating a user we need to make sure it belongs to the current tenant. This means you will probably have to write a custom authentication backend.

Applications implementing the row-level multitenancy model can face performance degradation due to the large volume of data. Great attention needs to be paid when writing queries in order to avoid data leaks.

Types of multitenant applications

There are several types of multitenant web applications, each with its pros and cons:

  1. Row-Level Multitenancy: - Basically store data in the same database and make sure the isolation is enforced at application level
  2. Schema-Level Multitenancy: - Store data in the same database but in different schemas
  3. Database-Level Multitenancy: - Store data in different databases

As you can properly guess, the Row-Level Multitenancy setup provides the least isolation while the Database-Level Multitenancy provides the stronger. Unfortunately, the better the isolation the harder the application maintenance gets.

As you can properly guess, the Row-Level Multitenancy setup provides the least isolation while the Database-Level Multitenancy provides the stronger. Unfortunately, the better the isolation the harder the application maintenance gets.

For many non-enterprise level application, the Row-Level Multitenancy model is enough. In this tutorial we will build two mechanisms for facilitating data isolation:

  1. Tenant Selection: - A way of selecting the current tenant based on the hostname
  2. Data Ownership: - A way of querying data such that we only provide results belonging to the current tenant

Initial Django Multitenant Application Setup

Let's work around a TodoList application in this tutorial. Suppose we have these models:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from django.db import models


# Create your models here.
class TodoList(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name


class TodoListItem(models.Model):
    list = models.ForeignKey(
        TodoList,
        related_name="items",
        on_delete=models.CASCADE
    )
    title = models.CharField(max_length=100)
    description = models.TextField(blank=True)
    order = models.IntegerField()

    def __str__(self):
        return f"{self.list.name}:{self.title}"

    class Meta:
        ordering = ('order', )

We also have this simple view:

1
2
3
4
5
6
7
8
from django.shortcuts import render

from.models import TodoList


def index(request):
    todo_lists = TodoList.objects.all()
    return render(request, "todo/index.html", {"todo_lists": todo_lists})

That renders this simple template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    
</body>
</html>

I created a few models in the Django Admin so that we have something to work with. Here's how our web page looks like:

Non-multitenant Django Todo List Application
Non-multitenant Django Todo List Application

Setting up Multitenancy

Let's now transform this simple application into a multitenant one.

Setting up different subdomains

First, in order to have different subdomains on our local development environment, we need to edit /etc/hosts. Add the following 2 entries to the end of the file:

1
2
3
# ...
127.0.0.1	greywolf.local
127.0.0.1	orangefox.local

If you head over to these addresses in your browser you should see the same page as before. Let's now go a step forward and distinguish between tenants.

Setting up tenants

Let's create a simple Tenant model:

1
2
3
4
5
6
class Tenant(models.Model):
    name = models.CharField("Tenant Name", max_length=100)
    subdomain = models.SlugField("Subdomain")

    def __str__(self):
        return self.name

Make sure you run python manage.py makemigrations and python manage.py migrate. Register the new model in admin.py. Go into admin and create 2 Tenant models corresponding to the 2 domains we registered in out /etc/hosts.

Create Tenant Models
Create Tenant Models

Middleware for selecting current tenant

We need a way to distinguish between the tenants by looking at the hostname. We can get the hostname from the HttpRequest object and then get the associated tenant with the hostname.

This code needs to run on all requests coming into our application. The second desirable quality of this piece of code would be to provide a place for accessing the current tenant from everywhere in the application. For this we will use a trick: Thread local variables.

The best place for this code, is of course, in a middleware component. Let's write it in middleware.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import threading

from tenants.models import Tenant

_local = threading.local()


class TenantMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        _local.request = request
        host = request.get_host()
        subdomain = host.split(".")[0]

        try:
            request.tenant = Tenant.objects.get(subdomain=subdomain)
        except Tenant.DoesNotExist:
            request.tenant = None

        response = self.get_response(request)
        return response

    @staticmethod
    def get_current_request():
        return getattr(_local, 'request', None)

    @staticmethod
    def get_current_tenant():
        request = TenantMiddleware.get_current_request()
        if not request:
            return None
        return request.tenant

Don't forget to register the middleware component in settings.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'tenants.middleware.TenantMiddleware',  # <--
]

If you add a debug print and head over to your browser (if you used the same domains as me: greywolf.local and orangefox.local), you should see how the tenant attribute on the request object changes.

Setting up TenantOwnedModel

We now need to mark those models that belong to a tenant, and we also need a way of querying these models, preferably without explicitly filtering by tenant every time. This is perfect scenario for overriding the default QueryManager and for using Abstract Models:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class TenantOwnedManager(models.Manager):
    def get_queryset(self):
        from tenants.middleware import TenantMiddleware
        tenant = TenantMiddleware.get_current_tenant()
        if tenant is None:
            return super().get_queryset()
        return super().get_queryset().filter(owner=tenant)


class TenantOwnedModel(models.Model):
    owner = models.ForeignKey(
        Tenant,
        on_delete=models.CASCADE,
        null=True,
        blank=True
    )
    objects = TenantOwnedManager()

    class Meta:
        abstract = True

By using the abstract = True flag, we instruct Django not to create a table in the database. This model will only be used as a base for other models.

Notice how we got the current tenant using the TenantMiddleware class we created. If we've got such a tenant we filter the current QuerySet by owner.

Let's get back to out initial TodoList and TodoListItem models and have them extent the TenantOwnedModel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from django.db import models
from tenants.models import TenantOwnedModel


class TodoList(TenantOwnedModel):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name


class TodoListItem(TenantOwnedModel):
    list = models.ForeignKey(
        TodoList,
        related_name="items",
        on_delete=models.CASCADE
    )
    title = models.CharField(max_length=100)
    description = models.TextField(blank=True)
    order = models.IntegerField()

    def __str__(self):
        return f"{self.list.name}:{self.title}"

    class Meta:
        ordering = ('order', )

Make sure you run python manage.py makemigrations and python manage.py migrate. Go into admin and assign the TodoList and TodoListItem objects some owners.

Let's check our pplication again by visiting the two domains we've set up:

GreyWolf Tenant
GreyWolf Tenant
OrangeFox Tenant
OrangeFox Tenant

Success! The data has been correctly separated between the two tenants.

In this tutorial we implemented Row-Level Multitenancy in Django. We’ve created a nice and simple way to serve multiple tenants in a single application. By using a middleware component for the tenant selection ensures that every request is tenant-aware. Extending an AbstractModel for assigning ownership we've made it easy to associate data with a tenant. This approach allows for simple data separation while keeping the codebase nice and clean.