Using django-tenant-schemas

Supported versions

You can use django-tenant-schemas with currently maintained versions of Django – see the Django’s release process and the present list of Supported Versions.

It is necessary to use a PostgreSQL database. django-tenant-schemas will ensure compatibility with the minimum required version of the latest Django release. At this time that is PostgreSQL 9.3, the minimum for Django 1.11.

Creating a Tenant

Creating a tenant works just like any other model in Django. The first thing we should do is to create the public tenant to make our main website available. We’ll use the previous model we defined for Client.

from customers.models import Client

# create your public tenant
tenant = Client(domain_url='', # don't add your port or www here! on a local server you'll want to use localhost here
                name='Schemas Inc.',

Now we can create our first real tenant.

from customers.models import Client

# create your first real tenant
tenant = Client(domain_url='', # don't add your port or www here!
                name='Fonzy Tenant',
                on_trial=True) # migrate_schemas automatically called, your tenant is ready to be used!

Because you have the tenant middleware installed, any request made to will now automatically set your PostgreSQL’s search_path to tenant1, public, making shared apps available too. The tenant will be made available at request.tenant. By the way, the current schema is also available at connection.schema_name, which is useful, for example, if you want to hook to any of django’s signals.

Any call to the methods filter, get, save, delete or any other function involving a database connection will now be done at the tenant’s schema, so you shouldn’t need to change anything at your views.

Management commands

By default, base commands run on the public tenant but you can also own commands that run on a specific tenant by inheriting BaseTenantCommand.

For example, if you have the following do_foo command in the foo app:


from import BaseCommand

class Command(BaseCommand):
    def handle(self, *args, **options):

You could create a wrapper command by using BaseTenantCommand:


from import BaseTenantCommand

class Command(BaseTenantCommand):
    COMMAND_NAME = 'do_foo'

To run the command on a particular schema, there is an optional argument called --schema.

./ tenant_command do_foo --schema=customer1

If you omit the schema argument, the interactive shell will ask you to select one.


migrate_schemas is the most important command on this app. The way it works is that it calls Django’s migrate in two different ways. First, it calls migrate for the public schema, only syncing the shared apps. Then it runs migrate for every tenant in the database, this time only syncing the tenant apps.


You should never directly call migrate. We perform some magic in order to make migrate only migrate the appropriate apps.

./ migrate_schemas

The options given to migrate_schemas are also passed to every migrate. Hence you may find handy

./ migrate_schemas --list

migrate_schemas raises an exception when an tenant schema is missing.

migrate_schemas in parallel

Once the number of tenants grow, migrating all the tenants can become a bottleneck. To speed up this process, you can run tenant migrations in parallel like this:

python migrate_schemas --executor=parallel

In fact, you can write your own executor which will run tenant migrations in any way you want, just take a look at tenant_schemas/migration_executors.

The parallel executor accepts the following settings:

  • TENANT_PARALLEL_MIGRATION_MAX_PROCESSES (default: 2) - maximum number of processes for migration pool (this is to avoid exhausting the database connection pool)
  • TENANT_PARALLEL_MIGRATION_CHUNKS (default: 2) - number of migrations to be sent at once to every worker


To run any command on an individual schema, you can use the special tenant_command, which creates a wrapper around your command so that it only runs on the schema you specify. For example

./ tenant_command loaddata

If you don’t specify a schema, you will be prompted to enter one. Otherwise, you may specify a schema preemptively

./ tenant_command loaddata --schema=customer1


The command createsuperuser is already automatically wrapped to have a schema flag. Create a new super user with

./ createsuperuser --username=admin --schema=customer1


Prints to standard output a tab separated list of schema:domain_url values for each tenant.

for t in $(./ list_tenants | cut -f1);
    ./ tenant_command dumpdata --schema=$t --indent=2 auth.user > ${t}_users.json;


The storage API will not isolate media per tenant. Your MEDIA_ROOT will be a shared space between all tenants.

To avoid this you should configure a tenant aware storage backend - you will be warned if this is not the case.


MEDIA_ROOT = '/data/media'
MEDIA_URL = '/media/'

We provide which can be added to any third-party storage backend.

In your reverse proxy configuration you will need to capture use a regular expression to identify the domain_url to serve content from the appropriate directory.

# illustrative /etc/nginx/cond.d/tenant.conf

upstream web {
    server localhost:8080 fail_timeout=5s;

server {
    listen 80;
    server_name ~^(www\.)?(.+)$;

    location / {
        proxy_pass http://web;
        proxy_redirect off;
        proxy_set_header Host $host;

    location /media/ {
        alias /data/media/$2/;


There are several utils available in tenant_schemas.utils that can help you in writing more complicated applications.


This is a context manager. Database queries performed inside it will be executed in against the passed schema_name.

from tenant_schemas.utils import schema_context

with schema_context(schema_name):
    # All comands here are ran under the schema `schema_name`

# Restores the `SEARCH_PATH` to its original value

This context manager is very similiar to the schema_context function, but it takes a tenant model object as the argument instead.

from tenant_schemas.utils import tenant_context

with tenant_context(tenant):
    # All commands here are ran under the schema from the `tenant` object

# Restores the `SEARCH_PATH` to its original value

Returns True if a schema exists in the current database.

from django.core.exceptions import ValidationError
from django.utils.text import slugify

from tenant_schemas.utils import schema_exists

class TenantModelForm:
    # ...

    def clean_schema_name(self)
        schema_name = self.cleaned_data["schema_name"]
        schema_name = slugify(schema_name).replace("-", "")
        if schema_exists(schema_name):
            raise ValidationError("A schema with this name already exists in the database")
            return schema_name

Returns the class of the tenant model.


Returns the name of the public schema (from settings or the default public).


Returns the TENANT_LIMIT_SET_CALLS setting or the default (False). See below.


The optional TenantContextFilter can be included in settings.LOGGING to add the current schema_name and domain_url to the logging context.


    'filters': {
        'tenant_context': {
            '()': 'tenant_schemas.log.TenantContextFilter'
    'formatters': {
        'tenant_context': {
            'format': '[%(schema_name)s:%(domain_url)s] '
            '%(levelname)-7s %(asctime)s %(message)s',
    'handlers': {
        'console': {
            'filters': ['tenant_context'],

This will result in logging output that looks similar to:

[] DEBUG 13:29 django.db.backends: (0.001) SELECT ...

Performance Considerations

The hook for ensuring the search_path is set properly happens inside the DatabaseWrapper method _cursor(), which sets the path on every database operation. However, in a high volume environment, this can take considerable time. A flag, TENANT_LIMIT_SET_CALLS, is available to keep the number of calls to a minimum. The flag may be set in as follows:



When set, django-tenant-schemas will set the search path only once per request. The default is False.

Third Party Apps


Support for Celery is available at tenant-schemas-celery.


django-debug-toolbar routes need to be added to (both public and tenant) manually.

from django.conf import settings
from django.conf.urls import include
if settings.DEBUG:
    import debug_toolbar

    urlpatterns += patterns(
        url(r'^__debug__/', include(debug_toolbar.urls)),