15 minute guide to secure SaaS multitenancy with Django and Let's Encrypt
Software as a Service is eating the world. Many SaaS providers use subdomain-based multitenancy with an address scheme like "customername.mywebsite.com." This tutorial will get you started on how to implement a secure semi-isolated multitenant application using Django. With many cloud providers offering a free tier and free SSL certificates from Let's Encrypt, you can start your SaaS MVP with zero cash investment.
Why Django?
Let me tell you how I started using Django. Ten years ago, I left my job in order to start a small consulting business. At the time, RIA (Rich Internet Applications) like Siverlight and Flex were all the rage, so I picked an open source contender called OpenLazlo.
I made a nice proof of concept and landed my first client, but after the first month, it was clear I would not meet my deadlines using this technology stack. Boy, think of a wrong choice: I can bet most readers have never heard of this platform.
I googled "fast development web applications" and most entries were about Ruby on Rails with Django in a distant second place. I wasn't smart enough to make it through the Rails tutorial, but the Django tutorial went like a breeze.
So I started from scratch learning Python/Django along the way and it saved my business. Django has paid my rent for the last decade, so you can bet I'm biased — for me, it is really "the framework for perfectionists with deadlines."
Multi-tenancy strategies
There are typically three solutions for solving the multitenancy problem:
-
Isolated Approach: Separate databases where each tenant has its own database.
-
Semi Isolated Approach: Shared Database and separate schemas with one database for all tenants, but one schema per tenant.
-
Shared Approach: Shared Database and shared schema. All tenants share the same database and schema. There is a main tenant-table, where all other tables have a foreign key pointing to.
I'm using django-tenant-schemas and it is based on the strategy number two: semi isolated sharing the database but using one namespace (schema) for each client. This approach has a good compromise between security, simplicity, and performance.
-
Simplicity: barely make any changes to your current code to support multitenancy. Plus, you only manage one database.
-
Performance: make use of shared connections, buffers, and memory.
Each solution has its upsides and downsides. For a more in-depth discussion, see Microsoft’s excellent article on Multi-Tenant Data Architecture.
Basic setup for django-tenant-schemas
I will not waste space here talking about how to bootstrap a Django project as I can't possibly do it better then the project's documentation.
In order to use django-tenant-schemas, we must make a few changes in settings.py
. First, we change the database engine:
DATABASES = {
'default': {
'ENGINE': 'tenant_schemas.postgresql_backend',
# ...
}
}
Then we add a database router:
DATABASE_ROUTERS = (
'tenant_schemas.routers.TenantSyncRouter',
)
Next, we add the middleware tenant_schemas.middleware.TenantMiddleware
to the top of MIDDLEWARE_CLASSES
, so that each request can be set to use the correct schema:
MIDDLEWARE_CLASSES = (
'tenant_schemas.middleware.TenantMiddleware',
# ...
)
There are other middlewares available. Please refer to the docs for details. We also need a template context processor:
TEMPLATES = [
{
'BACKEND': # ...
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
# ...
'django.template.context_processors.request',
# ...
]
}
}
]
We must define which applications are shared (SHARED_APPS) and which applications are tenant-specific (TENANT_APPS) — we also must set the tenant model.
# at settings.py
SHARED_APPS = (
'tenant_schemas', # mandatory, should always be before any django app
'customers', # you must list the app where your tenant model resides in
'django.contrib.contenttypes',
# everything below here is optional
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.admin',
)
TENANT_APPS = (
'django.contrib.contenttypes',
# your tenant-specific apps
'myapp.hotels',
'myapp.houses',
)
INSTALLED_APPS = (
'tenant_schemas', # mandatory, should always be before any django app
'customers',
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.admin',
'myapp.hotels',
'myapp.houses',
)
TENANT_MODEL = "customers.Client"
At customers/models.py
, you define a model inheriting from TenantMixin
:
from django.db import models
from tenant_schemas.models import TenantMixin
class Client(TenantMixin):
name = models.CharField(max_length=100)
paid_until = models.DateField()
on_trial = models.BooleanField()
created_on = models.DateField(auto_now_add=True)
# default true, schema will be automatically created and synced when it is saved
auto_create_schema = True
Now you must create your app migrations for customers:
$ python manage.py makemigrations customers
The command migrate_schemas --shared
will create the shared apps on the public schema. Note: your database should be empty if this is the first time you’re running this command.
$ python manage.py migrate_schemas --shared
There are other optional steps, for example, if you want separate projects for the main website and tenants. Please check the documentation.
Getting a wildcard certificate
Let’s Encrypt is a free, automated, and open Certificate Authority. It is sponsored by a diverse group of organizations, from non-profits to Fortune 100 companies. They use a protocol called ACME (Automatic Certificate Management Environment).
With the version two of ACME, Let's Encrypt is offering wildcard SSL certificates for free — they are 500 USD/year or more at the typical commercial CA.
ACME V2 uses DNS for authentication. In the following example, I'm using DigitalOcean as my DNS provider, but there are other authentication plugins covering many popular platforms.
First, create an ini file containing your API key:
$ echo dns_digitalocean_token = 66906...your.key.here...864a > do-api.ini
The Let's Encrypt client is called certbot. Hopefully it will be just sudo apt install certbot
or sudo yum install certbot
in a couple weeks, but the most recent version has not hit your distro official package store, so you may have to get it from GitHub:
$ git clone https://github.com/certbot/certbot.git
$ cd certbot
$ ./certbot-auto --os-packages-only
$ ./tools/venv.sh
$ source venv/bin/activate
If there is a plugin for your DNS provider, getting a certificate is pretty easy:
$ certbot certonly --dns-digitalocean \
--dns-digitalocean-credentials do-api.ini \
--dns-digitalocean-propagation-seconds 60 \
-d '*.mywebsite.com' -d mywebsite.com \
--server https://acme-v02.api.letsencrypt.org/directory
If not, you will have to use the --manual
option and update your DNS records by hand.
Webserver settings
Unfortunately, deploying Django applications is not as easy as PHP — check the docs about Django deployment.
The basic virtual host configuration for Apache looks like the following:
<VirtualHost 127.0.0.1:8080>
ServerName mywebsite.com
ServerAlias *.mywebsite.com mywebsite.com
WSGIScriptAlias / "/path/to/django/scripts/mywebsite.wsgi"
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/mywebsite.com/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/mywebsite.com/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/mywebsite.com/fullchain.pem
</VirtualHost>
Conclusion
I hope you have enough information to get your secure multitenant web application up and running. I often answer questions at Stack Overflow so reach me there if you get stuck.
Hi Paulo, thanks for this
I can’t seem to wrap m head around the whole thing tho.
I attempted using nginx config, with that, i get a landing page but when ever i try to create a tenant, i am unable to access it, i get 502 bad gateway
been on it for a while now, I will appreciate your help
Hi Paulo, that’s really a very helpful article to get started with a multitenant application with Django. I want to use Amazon S3 for storing static and media content of tenants for faster delivery. Can you help how to do it?
Hi Paulo! Thanks for the post. Could you please help to answer the question on StackOverFlow as following this link: https://stackoverflow.com/questions/60349271/django-tenant-schemas-1-10-0-typeerror-tenanttutorialmiddleware-takes-no-arg