A lightweight, robust multi-tenancy solution for Django using SQLite. This project isolates tenant data into separate SQLite databases while maintaining a shared database for global data (tenants, users).
- Database Isolation: Each tenant has its own
sqlite3database file. - Admin Separation:
- Public Admin (
/admin/): Managing global entities (Tenants, Users). - Tenant Admin (
/r/<slug>/admin/): Managing tenant-specific data within the tenant's context.
- Public Admin (
- Strict Routing: Database router ensures tenant apps cannot write to the shared database and vice versa.
- Customizable: Configurable database locations, routing modes, and app sharing.
- Maintenance Mode: Built-in support for tenant-specific maintenance mode.
- Domain Support: Multiple domain aliases per tenant with primary domain management.
- Easy Tenant Switching: Context manager for tenant activation in scripts and Celery tasks.
-
Install Dependencies:
uv sync
-
Migrate System Database:
uv run python manage.py migrate
-
Create a Tenant:
python manage.py create_tenant amazon --name "Amazon" --domain "amazon.local"
-
Run Server:
python manage.py runserver
- Access Public Admin:
http://localhost:8000/admin/ - Access Tenant Admin:
http://localhost:8000/r/amazon/admin/
- Access Public Admin:
Configure these settings in core/settings.py to customize the behavior of the tenant system.
| Setting | Default | Description |
|---|---|---|
TENANT_MODEL |
Required | The dotted path to your Tenant model (e.g., "apps.tenant.CustomTenant"). |
DOMAIN_MODEL |
None |
The dotted path to your Domain model (e.g., "apps.tenant.Domain"). |
SHARED_APPS |
[] |
List of apps that live in the default (shared) database (e.g., auth, contenttypes). |
TENANT_APPS |
[] |
List of apps that live in the tenant databases (e.g., blog, tenant_users). |
TENANTS_DB_FOLDER |
"tenants" |
Folder path relative to BASE_DIR where tenant SQLite files are stored. |
| Setting | Default | Description |
|---|---|---|
TENANT_ROUTING_MODE |
"DOMAIN" |
How tenants are identified. Options: • "SUBFOLDER": /r/<slug>/• "DOMAIN": <slug>.domain.com |
TENANT_SUBFOLDER_PREFIX |
"r" |
Used with SUBFOLDER mode. The URL prefix (e.g., "r" results in /r/tenant/). |
TENANT_BASE_DOMAIN |
`"localhost" | Used withDOMAINmode. The base domain to strip when identifying tenants (e.g.tenant.example.com`). |
| Setting | Default | Description |
|---|---|---|
ROOT_URLCONF |
Required | URL config for the public/shared view (e.g., "core.urls_public"). |
TENANT_URLCONF |
None |
URL config for tenant-specific views (e.g., "core.urls_tenant"). Swapped automatically by middleware. |
| Setting | Default | Description |
|---|---|---|
AUTO_RUN_MIGRATION |
True |
Whether to automatically run migrations when creating a tenant. |
Your settings.py should segregate apps to ensure proper migration and routing:
SHARED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_sqlite_tenants",
"apps.tenant", # The app containing the Tenant model
]
TENANT_APPS = [
"apps.blog", # Content specific to a tenant
"apps.tenant_users", # Users specific to a tenant
]
# Combined for Django internals
INSTALLED_APPS = list(SHARED_APPS) + [
app for app in TENANT_APPS if app not in SHARED_APPS
]
DJANGO_TENANT_SQLITE = {
"TENANT_MODEL": "tenant.CustomTenant",
"DOMAIN_MODEL": "tenant.Domain",
"TENANT_URLCONF": "core.urls_tenant",
"TENANT_ROUTING_MODE": "DOMAIN",
"TENANT_SUBFOLDER_PREFIX": "r",
"TENANT_BASE_DOMAIN": "localhost:8000",
}
MIDDLEWARE = [
"django_sqlite_tenants.middlewares.TenantMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
DATABASE_ROUTERS = [
"django_sqlite_tenants.db_routers.TenantRouter",
]Creates a new tenant and initializes their database.
python manage.py create_tenant <slug> --name "Tenant Name" --domain "custom.domain.com"Arguments:
<slug>: Unique identifier for the tenant (required)--name: Display name (defaults to capitalized slug)--domain: Custom domain (optional)
Example:
python manage.py create_tenant apple --name "Apple Inc." --domain "apple.local"Runs migrations for tenant databases.
python manage.py migrate_tenant [--tenant <slug>]Options:
--tenant <slug>: Migrate only the specified tenant (optional, defaults to all tenants)
Examples:
# Migrate all tenants
python manage.py migrate_tenant
# Migrate specific tenant
python manage.py migrate_tenant --tenant amazon# apps/tenant/models.py
from django_sqlite_tenants.models import TenantMixin
class CustomTenant(TenantMixin):
# Add additional fields here
industry = models.CharField(max_length=100, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Tenant"
verbose_name_plural = "Tenants"# apps/tenant/models.py
from django_sqlite_tenants.models import DomainMixin
class Domain(DomainMixin):
# Add additional fields here
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "Domain"
verbose_name_plural = "Domains"from django.shortcuts import render
from apps.blog.models import Post
def blog_list(request):
# Automatically filters posts by current tenant
posts = Post.objects.all()
return render(request, 'blog/list.html', {'posts': posts})from apps.tenant.models import CustomTenant
def run_tenant_task():
tenant = CustomTenant.objects.get(slug='amazon')
with tenant:
# Code here runs in the context of 'amazon' tenant
# All database operations will use the tenant's database
posts = Post.objects.all()
print(f"Amazon has {len(posts)} posts")
# Back to public schema
print("Done with tenant context")python manage.py shell
>>> from apps.tenant.models import CustomTenant
>>> tenant = CustomTenant.objects.get(slug='amazon')
>>> tenant.activate()
>>> # Now all queries will use Amazon's database
>>> Post.objects.count()
5
>>> CustomTenant.deactivate() # Return to public schemaThe TenantRouter class handles all database routing:
- Shared apps always use the
defaultdatabase - Tenant apps use the current tenant's database
- Relations between shared and tenant models are allowed
- Migrations are automatically routed to the correct database
Set a tenant to maintenance mode:
from apps.tenant.models import CustomTenant
tenant = CustomTenant.objects.get(slug='amazon')
tenant.maintenance_mode = True
tenant.save()When in maintenance mode, all requests will return a 503 Service Unavailable response with a simple "System Under Maintenance" message.
DJANGO_TENANT_SQLITE = {
"TENANTS_DB_FOLDER": "data/tenants/databases",
# ... other settings
}DJANGO_TENANT_SQLITE = {
"TENANT_ROUTING_MODE": "SUBFOLDER",
"TENANT_SUBFOLDER_PREFIX": "tenants",
# ... other settings
}Tenant URLs would then be: /tenants/amazon/blog/
DJANGO_TENANT_SQLITE = {
"TENANT_URLCONF": "core.urls_tenant",
"ROOT_URLCONF": "core.urls_public",
# ... other settings
}from celery import shared_task
from apps.tenant.models import CustomTenant
from apps.blog.models import Post
@shared_task
def count_tenant_posts(tenant_slug):
tenant = CustomTenant.objects.get(slug=tenant_slug)
with tenant:
count = Post.objects.count()
return f"Tenant {tenant_slug} has {count} posts"An example project is included in the example/ directory that demonstrates:
- Tenant and Domain model implementations
- Blog application with tenant-specific content
- Tenant users management
- Admin interface customization
- Templates with tenant context
To run the example project:
cd example
uv sync
uv run python manage.py migrate
uv run python manage.py create_tenant amazon --name "Amazon" --domain "amazon.local"
uv run python manage.py runserverRun the test suite:
uv run pytestRun specific tests:
uv run pytest django_sqlite_tenants/tests.py -vContributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License.