How to Build Multi Tenants application with Django, Django Rest Framework and Django Tenant

Thinkitive Technologies
8 min readNov 9, 2023

--

What is Multi Tenants Application

In multi-tenant software architecture (also called software multi-tenancy), a single instance of a software application (and its underlying database and hardware) serves multiple tenants (or user accounts). A tenant can be an individual user, but more frequently, it’s a group of users (such as a customer organization) that shares common access to and privileges within the application instance. Each Tenant’s data is isolated from, and invisible to, the other tenants sharing the application instance, ensuring data security and privacy for all tenants.

Types of Multi Tenants Modeling

  1. Instance Replication Modeling:- In this modeling, we system spins a new instance for every Tenant. This is easier to start but hard to scale. It becomes challenging when there are more tenants. It’s similar to single-tenant architecture.
  2. Database Segregation Modeling:- In this modeling, we do separate databases for each Tenant to store their data in a specific database. Again this will become hard to handle databases when there is an increase in tenants.
  3. Schema Segregation Modeling:- In this modeling, we use a single database and a single instance of an application. When we create a new tenant, then create a new schema in that database for this Tenant to store their data separately.

Note:- In this blog, we will use Schema Segregation Modeling.

Why do we use multi-tenant applications?

Let’s take an example of the Healthcare Domain. In this domain, every client wants to separate and isolate their data from each other. The data in the Healthcare domain is susceptible to patients, so clients want to separate it. In the multi-tenant application, we separate the database or schema so that data will separate. So due to data-related compliance of clients, we are using a multi-tenant application.

What are Schemas?

Generally, a schema can be seen as a directory in an operating system, each directory (schema) with its own set of files (tables and objects). This allows the same table name and objects to be used in different schemas without conflict.

A database contains one or more named schemas, which in turn contain tables. Schemas also contain other named objects, including data types, functions, and operators. The same object name can be used in different schemas without conflict; for example, schema1 and my schema can contain tables named mytable. Unlike databases, schemas are not rigidly separated: a user can access objects in any schemas in the database they are connected to if they have the privileges to do so.

There are several reasons why one might want to use schemas:

  • To allow many users to use one database without interfering with each other.
  • Organize database objects into logical groups to make them more manageable.
  • Third-party applications can be put into separate schemas so they do not collide with the names of other objects.

Schemas are analogous to directories at the operating system level, except that schemas cannot be nested.

For More Information:- https://www.postgresql.org/docs/14/ddl-schemas.html

Requirements

  1. Python (v3.10) — https://www.python.org/
  2. PostgreSQL (v14) — https://www.postgresql.org/
  3. Django (v4.0) — https://www.djangoproject.com/
  4. Django Rest Framework (v3.14.0) — https://www.django-rest-framework.org/
  5. Django Tenants (v3.5.0) — https://django-tenants.readthedocs.io/en/latest/
  • Python — Python is an easy-to-learn, powerful programming language. It has efficient high-level data structures and a simple but effective approach to object-oriented programming.
  • PostgreSQL — PostgreSQL is a powerful, open-source object-relational database system for persisting data.
  • Django — Django is a high-level Python web framework encouraging rapid development and clean, pragmatic design. Built by experienced developers, it takes care of the hassle of web development, so you can focus on writing your app without reinventing the wheel. It’s free and open source.

Installation:- pip install Django==4.0

  • Django Rest Framework — Django REST framework is a powerful and flexible toolkit for building Web APIs.
    Installation:- pip install djangorestframework==3.14.0
  • Django Tenant — Django Tenant is a package; by using this, we will apply multi-tenancy in our application.
    Installation:- pip install Django-tenants==3.5.0

Note:- There is a problem in this package that this package currently supports sub-domain-based multi-tenancy. In addition, this may again make it hard to maintain lots of subdomains

But we will modify this package to multi-tenancy using one single domain.

Notes:-

  • This application enables Django-powered websites to have multiple tenants via PostgreSQL schemas. A vital feature for every Software-as-a-Service website.
  • Django currently provides no simple way to support multiple tenants using the same project instance, even when only the data is different. Because we don’t want you running many copies of your project, you’ll be able to have the following:
  • Multiple customers running on the same instance
    – Shared and Tenant-Specific data
    – Tenant View-Routing (Accept Tenant via Header and then using routing)

Create Multi-Tenancy Application

1. Create a basic Django application

The below command is used to create a Django project with name django_multi_tenancy
Django-admin start project django_multi_tenancy

2. Create an app

The below command is used to create a Django application with the name app
– python manage.py start app

3. Create two model

a. Tenant Model — This model is responsible for storing tenants’ names. You can add more fields if required in your case.
b. Domain Model — This model stores tenants’ domains unique for each Tenant.

From Django.Db import models
from django_tenants.models import DomainMixin, TenantMixin
class Tenant(TenantMixin):
name = models.CharField(max_length=100, unique=True)
#default true, the schema will be automatically created and synced when it is saved
auto_create_schema = True
class Domain(DomainMixin):
Pass

4. Basic Settings

– You’ll have to make the following modifications to your settings.py file.
– This setting is required for application multi-tenancy in application to handle schema-based multi-tenancy.
– Your DATABASE_ENGINE setting needs to be changed to
DATABASES = {
“default”: {
“ENGINE”: “django_tenants.postgresql_backend”,
#..
}
}

5. Add django_tenants.routers.TenantSyncRouter to your DATABASE_ROUTERS setting so that the correct apps can be synced, depending on what’s being synced (shared or Tenant)

This router is responsible for handling routing for a specific tenant. When we change the schema based on the Tenant, then this will handle that situation. We will use routing to switch to a specific tenant schema.
DATABASE_ROUTERS = (
“django_tenants.routers.TenantSyncRouter”,
)

6. Create a middleware.py file and add this code

This is one of the critical files and codes where we will check and verify the Tenant and switch the schema based on the Tenant. When we send the tenant name from Tenant-Header, then in middleware, find that name and validate for the correct tenant name. If this is correct, we change the schema to that specific Tenant. The code below is responsible for this logic.

From django.conf import settings
from django.core.exceptions import DisallowedHost
from django.http import HttpResponseNotFound
from django.db import connection
from django.http import Http404, JsonResponse
from django.urls import set_urlconf
from django.utils.deprecation import MiddlewareMixin
from django_tenants.utils import (
get_public_schema_name,
get_public_schema_urlconf,
get_tenant_types,
has_multi_type_tenants,
remove_www,
)
from app.models import Tenant
class TenantMainMiddleware(MiddlewareMixin):
TENANT_NOT_FOUND_EXCEPTION = Http404
@staticmethod
def hostname_from_request(request):
return remove_www(request.get_host().split(“:”)[0])
def process_request(self, request):
connection.set_schema_to_public()
try:
hostname = self.hostname_from_request(request)
except DisallowedHost:
return HttpResponseNotFound()
#Check the Tenant from headers to change the schema for each request.
tenant_name = request.headers.get(“Tenant-Header”)
try:
tenant = Tenant.objects.get(name__iexact=tenant_name)
except Tenant.DoesNotExist:
if tenant_name != “public”:
return JsonResponse({“detail”: “Tenant not found”}, status=400)
self.no_tenant_found(
request, hostname
) # If no tenant is found, then set to public Tenant and return
return
tenant.domain_url = hostname
request.tenant = tenant
connection.set_tenant(request.tenant)
self.setup_url_routing(request)
def no_tenant_found(self, request, hostname):
if (hasattr(settings, “SHOW_PUBLIC_IF_NO_TENANT_FOUND”) and
settings.SHOW_PUBLIC_IF_NO_TENANT_FOUND):
self.setup_url_routing(request=request, force_public=True)
else:
raise self.TENANT_NOT_FOUND_EXCEPTION(‘No tenant for hostname “%s”‘
% hostname)

@staticmethod
def setup_url_routing(request, force_public=False):
public_schema_name = get_public_schema_name()
if has_multi_type_tenants():
tenant_types = get_tenant_types()
if not hasattr(request, “tenant”) or (
(force_public or request. Tenant.schema_name ==
get_public_schema_name())
and “URLCONF” in tenant_types[public_schema_name]):
request.urlconf = get_public_schema_urlconf()
else:
tenant_type = request. Tenant.get_tenant_type()
request.urlconf = tenant_types[tenant_type][“URLCONF”]set_urlconf(request.urlconf)
else:
#Do we have a public-specific urlconf?
if hasattr(settings, “PUBLIC_SCHEMA_URLCONF”) and (
force_public or request. Tenant.schema_name ==
get_public_schema_name()
):
request.urlconf = settings.PUBLIC_SCHEMA_URLCONF

Note:- Here, we are using public for the default tenant.

7. Add the middleware app.middleware.TenantMainMiddleware to the top of MIDDLEWARE so that each request can be set to use the correct schema.

This code is responsible for adding the above middleware class into the Django application, so when a request comes, then first, this middleware will call and, according to the tenant schema, will be changed.

MIDDLEWARE = (
“app.middleware.TenantMainMiddleware”,
#…
)

8. Add the context processor django.template.context_processors.request context_processors option of TEMPLATES; otherwise, the Tenant will not be available on request.

The code below is responsible for adding a tenant to the request object so that in our request, we can find the Tenant and set the Tenant in the request.

TEMPLATES = [
{
#…
‘OPTIONS’: {
‘context_processors’: [
‘django.template.context_processors.request’,
#…
],
},
},
]

9. Admin Support (for Django Super Admin)

TenantAdminMixin is available to register the tenant model. The mixin disables save and deletes buttons when not in the current or public Tenant (preventing Exceptions).

from django.contrib import admin
from django_tenants.admin import TenantAdminMixin
from app.models import Domain, Tenant
# Register domain model
admin.site.register(Domain)
# Register tenant model
@admin.register(Tenant)
class TenantAdmin(TenantAdminMixin, admin.ModelAdmin):
list_display = (‘name’, )

10. Create one of our use case models

We will create a sample model that will be Tenant-specific and then verify whether or not our implementation multi-tenancy is applied in our application. I will use this model as TENANT APPS.

From django.db import models
class Hotel(models.Model):
name = models.CharField(max_length=255)
location = models.CharField(max_length=255)
description = models.CharField(max_length=255)
picture = models.ImageField()

11. Configure Tenant and Shared Applications

To use shared and tenant-specific applications, there are two settings called SHARED_APPS and TENANT_APPS. SHARED_APPS is a list of strings like INSTALLED_APPS and should contain all apps you want to be synced to the public. However, if SHARED_APPS is set, these are the only apps syncing to your public schema! The same applies to TENANT_APPS, which expects a tuple of strings where each string is an app. If set, only those applications will be synced to all your tenants. Here’s a sample set. THIRD_PARTY_APPS and default django app will add as SHARED_APPS, and our use case model (Like the Hotel model) is TENANT_APPS

THIRD_PART_APPS = [
# Third party apps
]SHARED_APPS = [
‘django_tenants’,
‘django.contrib.contenttypes’,
‘django.contrib.auth’,
‘django.contrib.sessions’,
‘django.contrib.sites’,
‘django.contrib.messages’,
‘django.contrib.admin’,
] + THIRD_PART_APPS
TENANT_APPS = [
# your tenant-specific apps
‘app.Hotel’,
]
INSTALLED_APPS = SHARED_APPS + [app for app in TENANT_APPS if app not in SHARED_APPS]

12. You also have to set where your tenant & domain models are located.

Below lines are required to….Continue Reading

--

--

Thinkitive Technologies

By our innovative & creative technology services & solutions. We provide end-to-end healthcare offering across Consulting, Engineering, Testing & Analytics.