How to Use django-tenant-schemas to Create a Multi-Tenant Application

Pablo Grill
May 15, 2020

A multi-tenant application (or multitenancy software) is an architecture in which a single instance serves multiples tenants. In other words, we host our application in a single place and all our customers (tenants) use the same resources. This architecture is an alternative to the traditional architecture in which each tenant hosts a dedicated instance. The following image depicts the difference between both approaches.

Graphic by Lakshay Suri.
Graphic by Lakshay Suri.

Choosing between a traditional architecture or a multi-tenant architecture is not a simple decision. Sometimes there are some company rules that make the multi-tenant option unviable (for example, the company requires the app to be hosted in their servers). If this is not the case, there are some advantages to the multi-tenant approach:

  • Easier version management. In a single-tenant approach, every time a version of the application changes, you need to upgrade all instances. In a multi-tenant approach, you have a single instance to upgrade.
  • Cheaper resources. The number of resources that you need to host a multi-tenant app (CPU, memory, etc) are usually fewer than when you host the same application in independent instances. Moreover, if you manage the resources in a smart way (using an orchestrator like Kubernetes, for example) you can save a lot of resources.
  • Greater facility in sharing information between tenants. Sometimes, there is public information that all the tenants should be able to access. Managing this kind of information is easier with a multi-tenant approach than with a traditional approach.

There are several types of applications that can benefit from a multi-tenant architecture. Some of the clearest examples are SaaS (Software as a Service) products.

Databases in Multi-Tenant Apps

Multi-tenant applications have some complex issues to deal with. The main characteristic of this architecture is the ability to share resources, a task that brings up multiple challenges such as resource distribution, security, customizations, etc. In this post, we will just focus on database design.

In a multi-tenant application, resources are shared between tenants but usually each tenant's information is private. This means that we must ensure that their information is secure and only accessible to their members. There are multiple ways to implement this, but we will list only 3 of them.

Multiple Database Instances

This is maybe the safest option in terms of privacy and security. In this approach, each tenant has a standalone database instance where their data is stored. The application server must be connected to each of these instances and determine which database must be activated for each user(based on the tenant to which the user belongs).

Some advantages of this approach:

  • The data is secure and private because it is stored in independent databases. Each tenant only has access to their own database, keeping the other tenant’s information private.
  • Other tenants never connect to your database, resulting in better performance. As we mentioned earlier, resource distribution between tenants is a challenging task. Having different databases removes the database connections as a shared resource.

Some disadvantages:

  • Having multiple database instances means higher costs. As we mentioned in the introduction, one of the main advantages of multi-tenant architecture is the ability to share resources and reduce costs. In this approach, the database is no longer a shared resource.
  • Adding new tenants requires new connections to new databases. Implementing this approach requires an independent connection between the abbreviation server and each one of the databases. That means that when a new tenant is added, a new database must be created and connected to the application server. This connection can generate a downtime in the application server that can affect the rest of the tenants.
  • The management of each database is independent. All the management tasks, such as backups and vacuuming, must be done per instance.
  • Having shared data between tenants is not trivial. In this approach, all the shared data must be replicated in all the databases.

Single Database, Single Schema

This situation is the opposite of the previous one. In this case, all tenants’ information is stored in the same database schema, using the same tables. Each table of this schema has an attribute that identifies the tenant owner of that piece of information (for example, a tenant_id column). The application server is responsible for filtering by tenant each time that a request is made.

Advantages of this approach:

  • All data is stored in a single instance. The configuration of this architecture is exactly the same as a standalone application, making all database management tasks easier (backups, vacuuming the instance, etc).
  • A single connection exists between the database and the application server. Adding new tenants doesn’t require any configuration changes, making it a trivial task.
  • Sharing data between tenants is easy. You only need to store that data in a table without the tenant_id column.

On the other hand, the approach also has disadvantages:

  • The application server always needs to filter by tenant_id before accessing the data. If for some reason the filter is not applied, a user can access information from a tenant of which he is not a member. This is a privacy vulnerability because a single error that a developer might make in a component can compromise sensitive information.
  • The whole performance of the app can be affected by a single tenant. As we have a single database instance and all the tables are shared between tenants. For example, if a user tries to execute a bulk operation over a huge table that requires locking it, the table will be locked for all tenants. This means that the usability of the application can be affected because a single tenant is applying a bulk operation.

Single Database, Multiple Schemas

This option is a kind of hybrid of the previous ones. In this case, we have a single database instance to store all the tenants’ information. The difference between this approach and the previous one is that we create a separate schema per tenant. When the application server needs to access the data, the schema is activated based on the user and the tenant they belong to.

This approach has some of the same advantages as the previous ones:

  • The data is secure and private because it is stored in independent schemas.
  • All data is stored in a single instance, reducing costs and configurations and management tasks.
  • Adding new tenants only involves the creation of new schemas. The connection to the database is shared, so no configuration changes are required.
  • Sharing data between tenants is easy. All shared data can be stored in a public schema that all tenants can access.

However, this approach also has some limitations and disadvantages:

  • The whole performance of the app can be affected by a single tenant. If a single user starts to create new connections to the database instance, all tenants will detect a slowness in the application. However, the block issue mentioned earlier doesn’t exist because the tables are stored in different tenants.
  • Changing the structure of the tables requires modifying multiple schemas. This is a management task that can be a little bit tedious and complex.

Implementing Multitenancy in a Django application

Now that we have defined what a multi-tenant application is and the different approaches to designing the architecture of the database, let’s see how to implement a multi-tenant application in Django.

Why Use the django-tenant-schemas Library?

The first issue is that Django doesn’t provide a default way to deal with multiple tenants. In our experience working with Django, the best way to resolve that limitation is to use the package django-tenant-schemas.

Some of the reasons that justify this decision are:

  • It is a solution for multitenancy using the "single database, multiple schemas" approach. This is the approach that we think has the most advantages and can be used in more situations.
  • It is very easy to integrate with an existing Django app. The solution proposed for this package doesn’t affect any of your applications. In contrast to other alternatives that force the developer to modify all the apps, this package requires only a small configuration and is ready to be used.
  • The library resolves the routing between tenants. As we mentioned earlier, we need to activate the corresponding schema based on the tenant that the user is working on. This library has already implemented a solution that helps us in this task.
  • Finally, the library also supports shared data between tenants.

An Application Example

Let’s see how we can easily convert a Django application into a multi-tenant application with a simple example using django-tenant-schemas. Imagine that we are building a product for the transport industry that consists of assigning drivers to shipments.

At the beginning of the project, we didn’t think that the product would scale, so we created a single Django application with 3 apps.

Locations

App responsible for the storage of all the locations. The main model of this app is Location and looks like this:

from django.db import models

class Location(models.Model):
    name = models.CharField(max_length=255, unique=True)
    address = models.CharField(max_length=255) 
    latitude = models.FloatField()
    longitude = models.FloatField()

Drivers

App responsible for the storage of all the drivers. The main model of this app is Driver and looks like this:

from django.db import models

class Driver(models.Model):
    name = models.CharField(max_length=255, unique=True)

Shipments

App responsible for the storage of all the shipments. The main model of this app is Shipment and looks like this:

from django.db import models

from locations.models import Location
from drivers.models import Driver

class Shipment(models.Model):
     origin = models.ForeignKey(Location, on_delete=models.CASCADE)
     destination = models.ForeignKey(Location, on_delete=models.CASCADE)
     driver = models.ForeignKey(Driver, on_delete=models.CASCADE)
     completion = models.DateTimeField()

Imagine now that after some time the solution works great, several companies want to use it, and the simple product becomes a SaaS solution. This situation is clearly a use case for multitenancy architecture. Let’s convert this simple app into a multi-tenant app.

Installing the Library

Obviously, the first step is to install the django-tenant-schemas package using pip.

pip install django-tenant-schemas

Configuring the Application to Work with Tenants

The second step is configuring the application to work with tenants. The advantage of using the django-tenant-schemas package is that we just need to modify the django settings file.

Let’s assume for now that we will identify the tenant to work from the url (using subdomains, for example).

The changes that we need to implement in this file are:

  1. Change the DATABASE_ENGINE:
DATABASES = {
    'default': {
        'ENGINE': 'tenant_schemas.postgresql_backend',
        # ..
    }
}
  1. Add tenant_schemas.routers.TenantSyncRouter to your DATABASE_ROUTERS:
DATABASE_ROUTERS = (
    'tenant_schemas.routers.TenantSyncRouter',
)
  1. As we assume that the tenants are identified by the url, we can use the middleware that the package provides to correctly redirect to the schemas.
MIDDLEWARE_CLASSES = (
    'tenant_schemas.middleware.TenantMiddleware',
)
  1. Create and declare a tenant model. In every multi-tenant application we need to specify in some place which are the different tenants we work with. In this case, we need to create a model that inherits from_ TenantMixin _and declare it in the settings file as the TENANT_MODEL. In our example, we will create a new app called organizations and a tenant model called Organization.
from django.db import models
from tenant_schemas.models import TenantMixin

class Organization(TenantMixin):
    name = models.CharField(max_length=100)
TENANT_MODEL = "organizations.Organization"
  1. Split your apps into shared or tenant apps. With this package, the difference between a tenant model or a shared model is defined at app level. The declaration of them is done in the settings files. The difference between shared and tenant apps is that the shared apps will be stored in the public schema (accessible for all tenants), while the tenant apps will have an independent schema. In our example, we can assume that the locations are public, so we can manage this app as a shared one. On the other hand, drivers and shipments store information that is private form each tenant, so we need to declare them as tenant applications. Given these assumptions, the settings file looks like this:
SHARED_APPS = (
    'tenant_schemas',  # mandatory, should always be before any django app
    'organizations', # you must list the app where your tenant model resides in
    'locations',
    'django.contrib.contenttypes',
)

TENANT_APPS = (
    'django.contrib.contenttypes',
    # your tenant-specific apps
    'drivers',
    'shipments',
)

INSTALLED_APPS = (
    'tenant_schemas',  # mandatory, should always be before any django app

    'organizations',
    'django.contrib.contenttypes',
    'django.contrib.auth',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.admin',
    'locations',
    'drivers',
    'shipments',
)

After these configuration steps, you will have everything ready to deploy and execute your application in a multi-tenant architecture. The only consideration that you need to take is the migrations. Django's default migrate command will create all the tables in the public schema, making your application fail each time that you try to access a tenant application. To deploy the application correctly you need to use the command migrate_schema.

Limitations of the Package

After working with this package, the only limitation that we found is that it only works using Postgres.

As we have shown, creating a multi-tenant application in Django is not complex, giving you the possibility of implementing a SaaS solution without problems. Our example is the most basic and simplest usage of the django-tenant-schemas package. The package has more functionalities that allow you to create more complex solutions than shown here (for example, you can customize your own middleware and choose the tenant not only from the url). For more information, see the official documentation. We hope that this post helps you better understand what a multi-tenant application is and how you can build one in Django.

"How to Use django-tenant-schemas to Create a Multi-Tenant Application" by Pablo Grill is licensed under CC BY SA. Source code examples are licensed under MIT.

Photo by Matthew T. Radar. Graphic by Lakshay Suri.

Categorized under research & learning.

We are Sophilabs

A software design and development agency that helps companies build and grow products by delivering high-quality software through agile practices and perfectionist teams.