Adding Basic Multi-Tennancy to Django Sites Using Subdomains

Put subdomains to work and build dynamic multi-tennant applications.

Single domain web sites are so boring. Custom subdomains are a great way to provide customization for customers of SaaS products and to differentiate content without resorting to long URL paths. Implementing subdomain support in a web application isn’t

Subdomain-based redirection

The most efficient way to redirect requests based on subdomains is to use your front-end webserver, e.g. Nginx. This works well when you have only a few domains to redirect or when you know up front what all of those subdomains are. If you don’t know what this list includes, there’s a multitude, or you want someone else like a site manager to be able to update these, then it’s time to take a look at what the application layer can do.

In this example, we have a simple model which includes a subdomain and a URL to which that subdomain request should redirect. The subdomain entries have a unique subdomain name but the URL need not be, as we may want multiple entires redirecting to the same place.

class Subdomain(models.Model):
    """A model for managing subdomains and the URLs to which they redirect."""
    name = models.SlugField(max_length=200, unique=True)
    url = models.CharField(max_length=400, verbose_name="URL")

The redirection itself is handled by a small piece of middleware, a simplified version of which is shared below.

class RedirectMiddleware(object):
    """Middleware class that redirects non "www" subdomain requests to a
    specified URL or business.
    """
    def process_request(self, request):
        """Returns an HTTP redirect response for requests including non-"www"
        subdomains.
        """
        scheme = "http" if not request.is_secure() else "https"
        path = request.get_full_path()
        domain = request.META.get('HTTP_HOST') or request.META.get('SERVER_NAME')
        pieces = domain.split('.')
        subdomain = ".".join(pieces[:-2]) # join all but primary domain
        default_domain = Site.objects.get(id=settings.SITE_ID)
        if domain in {default_domain.domain, "testserver", "localhost"}:
            return None
        try:
            route = Subdomain.objects.get(name=subdomain).url
        except Subdomain.DoesNotExist:
            route = path
        return HttpResponseRedirect("{0}://{1}{2}".format(
            scheme, default_domain.domain, route))

The full domain is broken into its component parts and the primary domain is ignored. Notice that this does assume your top-level domain has one component plus the top-level domain. The typical redirect response, e.g. return redirect('/myurl/') includes only the absolute URL, and since the goal here is to redirect to an entirely new domain it’s important to include the full path of the redirect. The middleware uses the get_full_path method to ensure that the path plus any query paramters are included in the redirect.

A note: if you’re using a deeper domain(s) like mydomain.co.uk then you’d need to adjust how you slice the domain.

Serving subdomain-based content

The real benefit of using subdomains in a web application comes from providing unique content based on the subdomain, like a customized microsite or customer store. There are literally tens (well, tens of thousands) of examples of this on the web, and the redirection middleware above gets us almost all the way there.

What we want to do is not redirect but provide different responses, so let’s look at an example. This code has some assumptions. It assumes that you have a set of URL patterns for a primary domain, perhaps the main application signup site, and then unique customer-based subdomains. It assumes that the URL configuration in myapp will provide the entire root URL configuration for the subdomain responses, and that the Account model in the myapp application is an identifier with a unique subdomain. The models have been left out in this example.

from django.conf import settings
from django.core.urlresolvers import resolve, Resolver404
from django.shortcuts import redirect

from myapp import urls as frontend_urls
from myapp.models import Account

class AccountIDMiddleware(object):

    def process_request(self, request):
        path = request.get_full_path()
        domain = request.META['HTTP_HOST']
        pieces = domain.split('.')
        redirect_path = "http://{0}{1}".format(
                settings.DEFAULT_SITE_DOMAIN, path)
        if domain == settings.DEFAULT_SITE_DOMAIN:
            return None
        try:
            resolve(path, frontend_urls)
        except Resolver404:
            try:
                # The slashes are not being appended before getting here
                resolve(u"{0}/".format(path), frontend_urls)
            except Resolver404:
                return redirect(redirect_path)
        try:
            account = Account.objects.get(subdomain=pieces[0])
        except Account.DoesNotExist:
            return redirect(redirect_path)
        request.account = account
        return None

Presuming you have added a new setting called DEFAULT_SITE_DOMAIN such as www.myapp.com, then the middleware will ignore all requests to that domain. For all other requests it checks to see if the URL pattern matches a pattern in the URL configuration for the subdomains (frontend_urls). If not, then it’s probably a bad link, and we redirect back to the primary domain.

If there’s a match, all the middleware does is add the right account to the request so that the views accessible via frontend_urls can use request.account to filter objects and content for each request, showing the right account name, users, page styles, etc.

Now go forth and leverage the synergistic powers of holistic subdomains.

blog comments powered by Disqus