Multitenancy w Django, czyli jak oddzielić klientów w jednej aplikacji

Utworzono: wrzesień 2023 Zaktualizowano: 1 października 2026

zadanie rekrutacyjne

Python, Django, django-tenants, PostgreSQL

Jedna aplikacja, wielu klientów i żaden nie może zobaczyć danych innego.

Każdy klient ma widzieć tylko swoje dane, pełna separacja. Wystarczy jedno zapytanie bez filtra po kliencie i dane jednej firmy lądują na ekranie drugiej.

Trudne jest to, że separacja musi działać wszędzie, nie tylko w widokach: w panelu admina, w zadaniach w tle, w raportach, w cache, w plikach wrzucanych przez użytkowników. Jak opiera się ją na tym, że każdy pamięta o filter(client=…), to ktoś w końcu zapomni.

Trzy drogi

Osobna baza na klienta daje najmocniejszą izolację i łatwo zrobić backup albo usunąć jednego klienta, tylko że każda baza to osobne połączenia, migracje i koszty, więc ma sens przy kilku dużych klientach. Wspólne tabele z kolumną klienta są najtańsze i skalują się do tysięcy klientów, ale izolacja zależy od kodu, więc filtr trzeba wymusić w jednym miejscu, menedżerem modeli, biblioteką django-multitenant albo Row Level Security w PostgreSQL. Pośrodku jest osobny schemat w PostgreSQL na klienta: jedna baza, każdy klient ma swój schemat z tymi samymi tabelami, a aplikacja przy każdym żądaniu przełącza search_path.

Wybrałem schematy

Osobny schemat na klienta z biblioteką django-tenants. Klient jest rozpoznawany po domenie, a reszta aplikacji pisze zwykłe zapytania, bez filtra po kliencie, więc nie ma czego zapomnieć. Minus jest taki, że migracje idą przez wszystkie schematy i przy bardzo wielu klientach robi się to wolne.

Konfiguracja to głównie podział aplikacji na wspólne i te, które każdy klient ma u siebie:

python
DATABASES = {
    "default": {"ENGINE": "django_tenants.postgresql_backend", "NAME": "app"},
}
DATABASE_ROUTERS = ("django_tenants.routers.TenantSyncRouter",)

MIDDLEWARE = [
    "django_tenants.middleware.main.TenantMainMiddleware",
    "django.middleware.security.SecurityMiddleware",
    # ...
]

SHARED_APPS = ["django_tenants", "customers", "django.contrib.contenttypes", "django.contrib.auth"]
TENANT_APPS = ["orders", "invoices"]
INSTALLED_APPS = SHARED_APPS + [app for app in TENANT_APPS if app not in SHARED_APPS]

TENANT_MODEL = "customers.Client"
TENANT_DOMAIN_MODEL = "customers.Domain"

Klient i jego domena to zwykłe modele:

python
from django.db import models
from django_tenants.models import DomainMixin, TenantMixin


class Client(TenantMixin):
    name = models.CharField(max_length=100)
    auto_create_schema = True


class Domain(DomainMixin):
    pass

Najłatwiej przeoczyć zadania w tle, bo tam nie ma żądania, z którego wiadomo, o jakiego klienta chodzi, więc schemat trzeba ustawić samemu:

python
from django_tenants.utils import tenant_context

from customers.models import Client


def send_monthly_invoices():
    for client in Client.objects.exclude(schema_name="public"):
        with tenant_context(client):
            ...  # tu zwykłe zapytania, już w schemacie klienta

Migracje puszcza się przez migrate_schemas, osobno dla części wspólnej (--shared) i dla schematów klientów.