Designing a multi-tenant Django app

Setting and getting the tenant Link to heading

Thread-Local Link to heading

Thread-Local using asgiref that is a drop-in replacement for threading.local with support to threads and asyncio.

from asgiref.local import Local

local = Local()


def set_tenant(id):
    local.id = id


def get_tenant():
    return getattr(local, "id", None)
  • Pros
    • Globally available
  • Cons

Marking models as tenant-aware Link to heading

Inheritance Link to heading

django-multitenant approach.

# models.py
class TenantModel(models.Model):
    objects = TenantManager()

    class Meta:
        abstract = True


class MyModel(TenantModel):
    pass
  • Pros
    • more flexibility
  • Cons
    • doesn’t work well for third party apps
    • the flaws of inheritance

Settings by apps Link to heading

django-tenants approach: https://django-tenants.readthedocs.io/en/latest/install.html#configure-tenant-and-shared-applications

# settings.py
SHARED_APPS = (
    ...
)

TENANT_APPS = (
    # tenant-specific apps
    ...
)

INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS]
  • Pros
    • work for third party apps
  • Cons
    • less flexibility

Settings by models (idea) Link to heading

Inspired by Settings by apps.

# settings.py
TENANT_MODELS = (
    'core.MyModel',
    ...
)

Decorator (idea) Link to heading

Inspired by attrs.

@tenant_aware
class MyModel(models.Model):
    objects = models.Manager()
  • Pros
    • more flexibility
    • no inheritance
  • Cons
    • doesn’t work well for third party apps

Applying the tenant Link to heading

Manager setting tenant in all queries Link to heading

Example:

# middleware.py
class TenantMiddleware:
    ...
    def __call__(self, request):
        set_tenant(extract_tenant(request))
        response = self.get_response(request)
        return response


# models.py
class TenantManager(models.Manager):
    def get_queryset(self):
        assert_tenant()
        return super().get_queryset().filter(tenant_id=get_tenant()})


class TenantModel(models.Model):
    objects = TenantManager()
    unsafe_objects = models.Manager()
  • Pros
    • No need to set the tenant ID in each query.
  • Cons
    • Admin doesn’t work because it will use objects without setting the tenant.
    • Tests will need more setup.

Manager setting tenant when available Link to heading

# middleware.py
class TenantMiddleware:
    ...
    def __call__(self, request):
        set_tenant(extract_tenant(request))
        response = self.get_response(request)
        return response


# models.py
class TenantManager(models.Manager):
    def get_queryset(self):
        qs = super().get_queryset()

        if tenant := get_tenant():
            assert_tenant()
            qs = qs.filter(tenant_id=tenant)

        return qs


class TenantModel(models.Model):
    objects = TenantManager()
  • Pros
    • Works with Django Admin
    • Tests better with tests
  • Cons
    • Less secure because some bug could let the query without the tenant.

Link to heading

References Link to heading