Evaluating multi-tenant architecture
Django framework is a mature framework with many packages that can add multitenancy. There are two main points to evaluate when building such a solution: how to store tenants’ data and which package to use.
Storing data for multiple tenants can be achieved in three different ways:
- create one database per tenant
- create one schema per tenant
- have all tenants share the same table(s)
Each solution has its pros and cons. There are many publications that discuss this in detail. Depending on how many tenants we are expecting or how much customization tenants need, we can choose one that suits us best. In our case, we have chosen “one schema per tenant”, which is a sensible middle ground.
Choosing a package requires evaluating:
- does it support chosen architecture
- is it production-ready
- does it have documentation
We have chosen django_tenants package as it meets all our needs. It’s using database schemas to save tenants’ data, is under active development with a stable release cycle, and has comprehensive documentation.
How does it work?
Every tenant has its own schema in the database. There is also one public schema for storing admin and shared apps. Tenants are identified by their hostnames. Multi-tenant middleware analyzes every request and matches the tenant's hostname with their schema. Then it sets a connection to use the tenant’s schema and all queries are run in the tenant context. If no tenant is found, a 404 error is raised.
How to build an admin panel
Writing a custom admin panel can be a daunting task requiring writing a lot of boilerplate code for CRUD operations. Instead, we can use Django to build an admin interface from models. It allows us to concentrate on adding additional functionality. The default tenant model contains only information about schema and domains associated with it. First, we can add fields for storing some more useful information about our tenants like their names, contact information, and activation status.
class Tenant(TenantMixin): full_name = models.CharField(max_length=255, default="") is_active = models.BooleanField(default=True, blank=True) created_on = models.DateField(auto_now_add=True) is_ready = models.BooleanField(default=False, blank=True) email = models.EmailField(blank=True) note = models.TextField(blank=True)
Field is_active can be used to temporary disable tenant and block access to their site. Modifying get_tenant method from TenantMiddleware allows us to check if tenant should be available.
from django_tenants.middleware.main import TenantMainMiddleware class TenantMiddleware(TenantMainMiddleware): def get_tenant(self, domain_model, hostname): tenant = super().get_tenant(domain_model, hostname) if not tenant.is_active: raise self.TENANT_NOT_FOUND_EXCEPTION("Tenant is inactive") if not tenant.is_ready: raise self.TENANT_NOT_FOUND_EXCEPTION("Tenant is not ready") return tenant
Displaying a list of tenants in the admin panel requires adding two more classes so that domains associated with the tenant appear inside its change form. It is also a good idea to add protection against accidentally removing a public schema that holds all information.
class DomainInline(admin.TabularInline): model = Domain extra = 0 @admin.register(Tenant) class TenantAdmin(TenantAdminMixin, admin.ModelAdmin): list_display = ( "name", "is_active", "is_ready", "created_on", ) inlines = [DomainInline] def has_delete_permission(self, request, obj=None): if obj is not None and obj.schema_name == get_public_schema_name(): return False return super().has_delete_permission(request, obj)
With the working admin panel, we can start adding fields that query data from tenant schema. To make it work, we need a way to change schema_name in the connection object. Using context manager from django_tenants.utils allows us to query data for every tenant independently by just passing tenant object, in this case from admin form:
def product_count(self, obj): with tenant_context(obj): return Product.objects.all().count()
The admin panel can be used to show all sorts of information about the performance of every shop. We can query and display the number of available products, orders placed in a given timespan, compute average order amount and spedition costs and so on.
Adding multitenancy to Saleor.io platform is not a complicated task. By choosing the right tools and utilizing the power of the Django admin panel, we are able to create a good starting point for building a multi-tenant ecommerce platform.