django-tenant-schemas

repository·master·Indexed 23 days ago

https://github.com/bernardopires/django-tenant-schemas

A Django library that enables multi-tenancy using PostgreSQL schemas, allowing a single project instance to isolate customer data into separate schemas while sharing common code and public data.

Tokens
15.9K
Snippets
43
Records
87
Agent score
78%

What's inside django-tenant-schemas

  1. How django-tenant-schemas handles multitenancy

    master

    The application implements a Semi Isolated Approach to multitenancy using PostgreSQL schemas. Instead of separate databases for each tenant, it uses a single database where each tenant has its own schema. This provides a balance between simplicity (managing one database) and performance (sharing connections, buffers, and memory).

    Tenant Identification

    Tenants are identified by their hostname (e.g., tenant.domain.com). This mapping is stored in a table within the public schema.

    Request Lifecycle

    1. A request arrives with a specific hostname.
    2. The application matches the hostname against the tenant table in the public schema.
    3. If a match is found, the PostgreSQL search_path is updated to include the tenant's specific schema.
    4. All subsequent database queries for that request are executed within the tenant's schema.
    5. If no matching tenant is found for the hostname, a 404 error is raised.

    Shared vs. Tenant-Specific Data

    • Tenant-Specific Applications: Most apps will reside in tenant-specific schemas, ensuring data isolation between customers.
    • Shared Applications: Apps that should be accessible to all tenants (e.g., public datasets) should have their tables located in the public schema. The application ensures these are always available by automatically adding the public schema to the search_path.
  2. How tenant routing works

    master

    The application identifies tenants using the request's hostname (e.g., tenant.domain.com).

    1. The hostname is matched against tenants stored in the public schema.
    2. If a match is found, the PostgreSQL search_path is updated to include the tenant's specific schema and the public schema.
    3. All subsequent database queries (e.g., .filter(), .get(), .save()) are automatically executed within the tenant's schema.
    4. If no matching tenant is found for the hostname, a 404 error is raised.

    This mechanism allows you to use the same Django views and logic while ensuring data isolation between customers.

  3. Untitled record

    master

    If your test suite becomes slow due to the overhead of creating and migrating schemas for every test, you can use FastTenantTestCase.

    Comparison:

    • TenantTestCase: Drops, recreates, and executes migrations for the test schema for every test case. This ensures isolation but is slow.
    • FastTenantTestCase: Creates the test schema and runs migrations only once. This is significantly faster but carries the risk of shared state between tests.
    from tenant_schemas.test.cases import FastTenantTestCase
  4. Configure Basic Settings for django-tenant-schemas

    master

    To integrate django-tenant-schemas into your Django project, update your settings.py with the following configurations:

    1. Database Engine: Change your default database engine to tenant_schemas.postgresql_backend.
    2. Database Router: Add tenant_schemas.routers.TenantSyncRouter to DATABASE_ROUTERS to ensure apps are synced to the correct schema (shared vs. tenant).
    3. Middleware: Add tenant_schemas.middleware.TenantMiddleware to the top of your MIDDLEWARE list. This ensures each request is routed to the correct schema based on the hostname.

    Middleware Options:

    • tenant_schemas.middleware.TenantMiddleware: Standard middleware. Returns 404 if the hostname doesn't match a valid tenant.
    • tenant_schemas.middleware.SuspiciousTenantMiddleware: Returns a 400 Bad Request/DisallowedHost error if the hostname is unrecognized.
    • tenant_schemas.middleware.DefaultTenantMiddleware: Serves the public tenant for unrecognized hostnames. You can subclass this to change the default schema.

    Note: Ensure django.template.context_processors.request is included in your TEMPLATES context processors.

    DATABASES = {
        'default': {
            'ENGINE': 'tenant_schemas.postgresql_backend',
            # ..
        }
    }
    
    DATABASE_ROUTERS = (
        'tenant_schemas.routers.TenantSyncRouter',
    )
    
    MIDDLEWARE = [
        'tenant_schemas.middleware.TenantMiddleware',
        # ...
    ]
  5. Implement centralized login with tenant redirection

    master

    A common pattern for centralized login:

    1. Host the login view on the public tenant (using PUBLIC_SCHEMA_URLCONF).
    2. After authentication, look up the user's associated tenant.
    3. Redirect the user to their tenant's domain.

    Warning on Session Cookies: If the public tenant and tenant subdomains differ, the session cookie might not be sent to the tenant.

    • Solution A: Set SESSION_COOKIE_DOMAIN to a shared parent domain (e.g., .example.com).
    • Solution B: Use a token-based redirect where the public login generates a one-time token for the tenant to consume.
    from django.contrib.auth import authenticate, login
    from django.shortcuts import redirect, render
    
    def login_view(request):
        if request.method == 'POST':
            user = authenticate(
                request,
                username=request.POST['username'],
                password=request.POST['password'],
            )
            if user is not None:
                login(request, user)
                tenant = get_tenant_for_user(user)  # your lookup logic
                return redirect(f'https://{tenant.domain_url}/')
        return render(request, 'login.html')
  6. Create a public tenant

    master

    Before creating specific tenants, you must create a public tenant to make your main website available. This tenant uses the schema_name='public' and should be created using your custom Client/Tenant model. Note that domain_url should not include ports or www prefixes.

    from customers.models import Client
    
    # create your public tenant
    tenant = Client(domain_url='my-domain.com', # don't add your port or www here!
                    schema_name='public',
                    name='Schemas Inc.',
                    paid_until='2016-12-05',
                    on_trial=False)
    tenant.save()
  7. Handle media URL mapping in production with Nginx

    master

    When using TenantFileSystemStorage, the file path on disk includes the tenant's domain_url, but the URL returned by FieldFile.url (and serialized by DRF) remains relative to MEDIA_URL (e.g., /media/uploads/photo.jpg).

    In production, you must configure your reverse proxy (like Nginx) to map these URLs back to the correct tenant subdirectory on disk by capturing the domain from the Host header.

    server {
        listen 80;
        server_name ~^(www\.)?(.+)$;
    
        location /media/ {
            alias /data/media/$2/;
        }
    
        location / {
            proxy_pass http://web;
            proxy_set_header Host $host;
        }
    }
  8. Implement custom tenant selection strategies

    master

    By default, django-tenant-schemas determines the tenant by extracting it from the URL (e.g., mytenant.mydomain.com) using TenantMiddleware.

    If you need alternative strategies—such as determining the tenant from an HTTP header, an OAuth token, or a fixed URL—you can implement custom middleware by subclassing BaseTenantMiddleware and implementing the get_tenant method.

    Implementation Requirements:

    1. Subclass BaseTenantMiddleware.
    2. Implement get_tenant(self, model, hostname, request):
      • model: The tenant model class (referenced as TENANT_MODEL).
      • hostname: The hostname of the current request.
      • request: The current Django request object.
    3. Return an instance of your TENANT_MODEL class.
    4. Middleware Order: Place your custom middleware at the top of your MIDDLEWARE list in settings.py.

    Note: You should only have one subclass of BaseTenantMiddleware per project. You can also extend existing middleware like TenantMiddleware to chain multiple strategies together by manipulating the super().get_tenant() call.

  9. Untitled record

    master

    To run the tests included with the library, execute the following command from the dts_test_project directory. This project is preconfigured with the necessary settings and apps to test both SHARED_APPS and TENANT_APPS environments.

    ./manage.py test tenant_schemas.tests

    To test all supported Django versions, use tox from outside the application.

    tox
  10. Create a real tenant

    master

    To create a new tenant, instantiate your tenant model with a unique schema_name and a domain_url. When you call .save(), migrate_schemas is automatically called for that tenant, making it ready for use. Once created, the tenant middleware will automatically set the PostgreSQL search_path to {tenant_schema}, public based on the request domain, and the tenant object will be available at request.tenant.

    from customers.models import Client
    
    # create your first real tenant
    tenant = Client(domain_url='tenant.my-domain.com', # don't add your port or www!
                    schema_name='tenant1',
                    name='Fonzy Tenant',
                    paid_until='2014-12-05',
                    on_trial=True)
    tenant.save() # migrate_schemas automatically called, your tenant is ready to be used!