Multitenant Systems - Part II - Building multitenant backend APIs using Django

Multitenant Systems - Part II - Building multitenant backend APIs using Django

This is a continuation of part I of the multitenant series. You can check part 1 here. In this one, I will discuss how we can build the backend of our app using the third architecture discussed in the part I.

What are we building?

An online shop app! This will be a multitenant app where a user can create their own online shop and add users and products to this shop. There can exist any number of shops in our app.

x1.png

A user can create a new shop and visit their shop using the shop code as the subdomain. Let's build the backend APIs for this system using the Django REST Framework.

Expected APIs

  • Create a shop
  • Create a user in this shop
  • Create a product in this shop
  • List the products in this shop

Database Design

We will have a table Tenant to store all the shops. All the other tables will have a reference to the Tenant table.

database design

Our Django Models

import uuid
from django.db import models
from django.contrib.auth.models import AbstractBaseUser

from .managers.tenant import TenantManager
from .managers.user import CustomUserManager


class Tenant(models.Model):

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=128)
    code = models.CharField(max_length=128, unique=True)


class TenantBaseModel(models.Model):

    class Meta:
        abstract = True

    objects = TenantManager()
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)


class User(AbstractBaseUser, TenantBaseModel):

    USERNAME_FIELD = 'id'
    REQUIRED_FIELDS = list()

    user_objects = CustomUserManager()
    first_name = models.CharField(max_length=512)
    last_name = models.CharField(max_length=512, null=True, blank=True)
    email = models.EmailField(max_length=512)

    class Meta:
        unique_together = ('tenant_id', 'email',)


class Product(TenantBaseModel):

    name = models.CharField(max_length=256)
    code = models.CharField(max_length=256)
    added_by = models.ForeignKey(User, on_delete=models.CASCADE)
    added_on = models.DateTimeField(auto_now_add=True)

TheTenant model on the top stores all the shop information.

The TenantBaseModel is an abstract class that can be inherited from all our tenant models like User and Product so they automatically get the tenant fields.

We have overridden the objects in the TenantBaseModel with the TenantManager which we will see next.

The TenantManager

from django.db import models


class TenantManager(models.Manager):

    def get_queryset(self):
        return None

    def t_filter(self, tenant, **kwargs):
        return super(TenantManager, self) \
            .get_queryset() \
            .filter(tenant=tenant, **kwargs)

We override the get_queryset to return None.

So, AnyTenantModel.objects.all() or AnyTenantModel.objects.filter() will return None.

We can use t_filter to get the single tenant related information, AnyTenantModel.objects.t_filter(tenant=tenant).

Tenant Routing Middleware

Every tenant-based API should have a tenant code passed. We can pass the tenant code via the API headers. This middleware will pick the tenant code from the headers, get the tenant object from the DB, and set the tenant in the request object.

from django.http import Http404
from django.urls import reverse

from ..models import Tenant

TENANT_URLS = [
    reverse('tenants-list')
]

class CustomTenantMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response
        self.TENANT_NOT_FOUND_EXCEPTION = Http404

    def __call__(self, request):
        code = request.META.get('HTTP_TENANT_CODE')
        if request.path in TENANT_URLS:
            return self.get_response(request)
        try:
            tenant = Tenant.objects.get(code=code)
            request.tenant = tenant
            return self.get_response(request)
        except Exception:
            raise self.TENANT_NOT_FOUND_EXCEPTION(
                f'Tenant with code \'{code}\' does not exists'
            )

Handling authentication

Once the JWT token authentication is done, we also check if this authenticated user is a part of this tenant.

from rest_framework import exceptions
from knox.auth import TokenAuthentication
from ..models import User


class CustomTokenAuthentication(TokenAuthentication):

    def authenticate(self, request):
        user, auth_token = super().authenticate(request)

        if user.tenant != request.tenant:
            raise exceptions.AuthenticationFailed(
                'User does not exist in this tenant')

        return user, auth_token

Tenant based viewsets

Once we have completed all the above steps, we can easily integrate all this together. Our authentication class will be the CustomTokenAuthentication class, we can set it globally in the settings file as well.

We override get_queryset to use t_filter and we get the tenant in the request object. So User.objects.t_filter(tenant=self.request.tenant) means all the users in this tenant.

from rest_framework import viewsets
from ..serializers.user import UserSerializer
from ..models import User
from ..authentication.auth import CustomTokenAuthentication


class UserViewSet(viewsets.ModelViewSet):

    serializer_class = UserSerializer
    authentication_classes = (CustomTokenAuthentication, )
    pagination_class = None

    def get_queryset(self):
        return User.objects \
            .t_filter(tenant=self.request.tenant) \
            .order_by('first_name')
from rest_framework import viewsets
from ..models import Product
from ..serializers.product import ProductSerializer
from ..authentication.auth import CustomTokenAuthentication


class ProductViewSet(viewsets.ModelViewSet):

    serializer_class = ProductSerializer
    authentication_classes = (CustomTokenAuthentication, )
    pagination_class = None

    def get_queryset(self):
        return Product.objects \
            .t_filter(tenant=self.request.tenant) \
            .order_by('code')

Tada! You have your multitenant backend APIs ready. Check out the full code here.

Thanks for reading!