Removing subdomains from a multitenant app

I have a working multitenant app on Django with isolated databases that currently uses a subdomain for each tenant and threading. The subdomain prefix is used to make a connection to the corresponding database, so:

And so on. This is the current code I have:

middleware:

import threading

from app.utils import tenant_db_from_the_request

Thread_Local = threading.local()


class AppMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        db = tenant_db_from_the_request(request)
        setattr(Thread_Local, 'DB', db)
        response = self.get_response(request)
        return response


def get_current_db_name():
    return getattr(Thread_Local, 'DB', None)

utils:

def hostname_from_the_request(request):
    return request.get_host().split(':')[0].lower()


def tenant_db_from_the_request(request):
    hostname = hostname_from_the_request(request)
    tenants_map = get_tenants_map()
    return tenants_map.get(hostname)


def get_tenants_map():
    return dict(Tenants.objects.values_list('subdomain', 'database',))

routers:

class AppRouter:

    def db_for_read(self, model, **hints):
        return get_current_db_name()

    def db_for_write(self, model, **hints):
        return get_current_db_name()

    def allow_relation(self, *args, **kwargs):
        return True

    def allow_syncdb(self, *args, **kwargs):
        return None

    def allow_migrate(self, *args, **kwargs):
        return None

middleware section on settings.py:

MIDDLEWARE = [
    'debug_toolbar.middleware.DebugToolbarMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'app.middleware.AppMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

Using subdomains is not exactly ideal, so I’m trying to switch the subdomain prefix to a suffix that will be added to the username of each user on the tenant. With that, every user will access the same URL:

mysite.com, for example

And the suffix will work like that:

  • user@client1 access the database client1
  • user@client2 access the database client2

Since I already have this working with subdomains, my thought process was to change just the middleware to retrieve the suffix of the username and use session to store the tenant alias that will be used on each database request. With that in mind, I changed just my utils file to this:

def hostname_from_the_request(request):
    # changed the request to get the 'db_alias' from the session
    return request.session.get('db_alias', None)


def get_tenants_map():
    # change the mapping to retrieve the database based on an alias (which is a column on the tenants table), instead of subdomain
    return dict(Tenants.objects.values_list('alias', 'database',))

And added these 3 lines of code before the authentication process in my login view:

if request.method == "POST":

    if form.is_valid():

        # retrieve the alias from the username
        complete_username = form.cleaned_data.get('username')
        db_alias = complete_username.split('@')[1]
        request.session['db_alias'] = db_alias
        
        # from here on, the code is unchanged
        user = authenticate(
            username=form.cleaned_data.get('username'),
            password=form.cleaned_data.get('password'),
        )

# rest of the code

This works, but only after the second login attempt. The first one always return a failed authentication error. With print statements and breakpoints I confirmed that the session is being created before the authentication and the name of the database is being retrieved correctly, so after the second login attempt, the login is successful and each request is processed on the corresponding database.

I’ve tried to use the session data directly instead of threading, but the problem with that is that the router doesn’t accept a request parameter, or at least it is what I understood from the docs, but I could be wrong.

Maybe I’m missing something obvious here, but I don’t understand why this is not working on the first login attempt. I appreciate any help.

Thanks!