Installation

This app requires:

  • Python (3.10+)

  • Django (5.0+)

  • Any compatible version of psycopg

You can install django-pgschemas via pip or any other installer.

pip install django-pgschemas

Basic Configuration

Use django_pgschemas.postgresql as your database engine. This enables the API for setting Postgres search path

DATABASES = {
    "default": {
        "ENGINE": "django_pgschemas.postgresql",
        # ...
    }
}

Add the middleware django_pgschemas.middleware.TenantMiddleware to the top of MIDDLEWARE, so that each request can be set to use the correct schema.

MIDDLEWARE = (
    "django_pgschemas.middleware.TenantMiddleware",
    # ...
)

Add django_pgschemas.routers.SyncRouter to your DATABASE_ROUTERS, so that the correct apps can be synced, depending on the target schema.

DATABASE_ROUTERS = (
    "django_pgschemas.routers.SyncRouter",
    # ...
)

Add the minimal tenant configuration.

TENANTS = {
    "public": {
        "APPS": [
            "django.contrib.contenttypes",
            "django.contrib.staticfiles",
            # ...
            "django_pgschemas",
            "shared_app",
            # ...
        ],
    },
    # ...
    "default": {
        "TENANT_MODEL": "shared_app.Client",
        "DOMAIN_MODEL": "shared_app.Domain",
        "APPS": [
            "django.contrib.auth",
            "django.contrib.sessions",
            # ...
            "tenant_app",
            # ...
        ],
        "URLCONF": "tenant_app.urls",
    }
}

Each entry in the TENANTS dictionary represents a static tenant, except for default, which controls the settings for all dynamic tenants. Notice how each tenant has the relevant APPS that will be synced in the corresponding schema.

Tip

public is always treated as shared schema and cannot be routed directly. Every other tenant will get its search path set to its schema first, then the public schema.

For Django to function properly, INSTALLED_APPS and ROOT_URLCONF settings must be defined. Just make them get their information from the TENANTS dictionary, for the sake of consistency.

INSTALLED_APPS = []
for schema in TENANTS:
    INSTALLED_APPS += [app for app in TENANTS[schema]["APPS"] if app not in INSTALLED_APPS]

ROOT_URLCONF = TENANTS["default"]["URLCONF"]

Creating tenants

More static tenants can be added and routed.

TENANTS = {
    # ...
    "www": {
        "APPS": [
            "django.contrib.auth",
            "django.contrib.sessions",
            # ...
            "main_app",
        ],
        "DOMAINS": ["mydomain.com"],
        "URLCONF": "main_app.urls",
    },
    "blog": {
        "APPS": [
            "django.contrib.auth",
            "django.contrib.sessions",
            # ...
            "blog_app",
        ],
        "DOMAINS": ["blog.mydomain.com", "help.mydomain.com"],
        "URLCONF": "blog_app.urls",
    },
    # ...
}

Dynamic tenants need to be created through instances of TENANTS["default"]["TENANT_MODEL"] and routed through instances of TENANTS["default"]["DOMAIN_MODEL"].

# shared_app/models.py

from django.db import models
from django_pgschemas.models import TenantModel
from django_pgschemas.routing.models DomainModel

class Client(TenantModel):
    name = models.CharField(max_length=100)
    paid_until =  models.DateField(blank=True, null=True)
    on_trial = models.BooleanField(default=True)
    created_on = models.DateField(auto_now_add=True)

class Domain(DomainModel):
    pass

Synchronizing tenants

As a first step, you must always synchronize the public schema in order to get the tenant and domain models created. You can then synchronize the rest of the schemas.

python manage.py migrate -s public
python manage.py migrate

Now you are ready to create your first dynamic tenant. In the example, the tenant is created through a python manage.py shell session.

>>> from shared_app.models import Client, Domain
>>> client1 = Client.objects.create(schema_name="client1")
>>> Domain.objects.create(domain="client1.mydomain.com", tenant=client1, is_primary=True)
>>> Domain.objects.create(domain="clients.mydomain.com", folder="client1", tenant=client1)

Now any request made to client1.mydomain.com or clients.mydomain.com/client1/ will automatically set Postgres’s search path to client1 and public, making shared apps available too. Also, at this point, any request to blog.mydomain.com or help.mydomain.com will set search path to blog and public.

This means that any call to the methods filter, get, save, delete or any other function involving a database connection will be done at the correct schema, be it static or dynamic.