Why Multi-Tenancy Matters for African Institutions
When I first started building LMS platforms, I made the classic mistake of building one app per client. By the third deployment, I knew something had to change.
Multi-tenancy lets you serve multiple institutions from a single Django codebase while keeping their data strictly isolated. Here's what I learned the hard way.
Schema-Level Isolation vs Row-Level Isolation
There are three common approaches:
1. Separate databases per tenant - maximum isolation, hardest to maintain.
2. Shared schema, tenant column on every table - simplest to build, easy to make mistakes.
3. Separate schemas (PostgreSQL only) - the sweet spot. Each tenant gets their own schema, Django's connection routes correctly, and you avoid the "missing WHERE clause" bug that exposes data across tenants.
I went with approach 3 for the IEF deployment and it's been rock solid.
The Django Configuration
# settings.py
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.getenv("DB_NAME"),
"SCHEMA": "public",
}
}
In a custom middleware, we set the search_path based on the request subdomain:
class TenantMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
tenant = get_tenant_from_host(request.get_host())
if tenant:
with connection.cursor() as cursor:
cursor.execute(f"SET search_path TO {tenant.schema_name}, public")
return self.get_response(request)
Lessons Learned
- Always validate schema names against an allowlist before injecting into raw SQL.
- Use
django-tenantsif you want a battle-tested library, but understand the internals before you use it. - Background tasks (Celery) need tenant context propagated explicitly - this catches people off guard.
- Migrations become more complex. Plan time for per-tenant migration runs.
This architecture now powers three live platforms serving over 2,000 active learners.