django-tenancy
Shared multi-tenancy infrastructure for the BF Agent platform — per ADR-035.
Core Components
- Organization: Tenant model with lifecycle management (trial → active → suspended → deleted)
- Membership: User-to-organization mapping with roles (owner, admin, member, viewer, external)
- SubdomainTenantMiddleware: Resolves tenant from subdomain or
X-Tenant-IDheader - TenantAwareManager: Explicit
.for_tenant(uuid)queryset filtering - Context propagation: Async-safe contextvars + PostgreSQL RLS session variable
- Health endpoints:
/livez/(liveness) +/healthz/(readiness with DB + Redis checks) - Decorators:
tenant_context()context manager,@with_tenant_from_arg()for Celery tasks
Installation
pip install -e ".[dev]"
Usage
# settings.py
INSTALLED_APPS = [
...
"django_tenancy",
]
MIDDLEWARE = [
...
"django_tenancy.middleware.SubdomainTenantMiddleware",
]
# urls.py
from django_tenancy.healthz import liveness, readiness
urlpatterns = [
path("livez/", liveness),
path("healthz/", readiness),
]
# In views / services:
from django_tenancy.managers import TenantAwareManager
class MyModel(models.Model):
tenant_id = models.UUIDField(db_index=True)
objects = TenantAwareManager()
# Query with tenant isolation:
MyModel.objects.for_tenant(request.tenant_id)
ADR-137: TenantManager (auto-filter)
from django_tenancy.managers import TenantManager
class MyModel(models.Model):
tenant_id = models.UUIDField(db_index=True)
objects = TenantManager()
# In request context → auto-filtered by middleware tenant:
MyModel.objects.all()
# Explicit (Celery, management commands):
MyModel.objects.for_tenant(tenant_uuid)
# Admin / cross-tenant reports:
MyModel.objects.unscoped()
ADR-137: Row-Level Security (Phase 2)
Setup (once per database)
# 1. Create separate DB roles
python manage.py setup_rls_roles --dry-run # preview
python manage.py setup_rls_roles \
--app-user=risk_hub \
--app-password=<secret>
# 2. Enable RLS on all tenant tables
python manage.py enable_rls --dry-run # preview
python manage.py enable_rls # execute
# Single table:
python manage.py enable_rls --table=risk_assessment
# Remove RLS:
python manage.py enable_rls --disable
DB Role Separation
| Role | Used by | RLS |
|---|---|---|
<db>_admin (table owner) |
migrate, createsuperuser | exempt |
<db>_app (non-owner) |
gunicorn, celery | active |
After setup_rls_roles, update DATABASE_URL for
gunicorn/celery to use the app-user. Keep the admin-user
for migrations.
ADR-137: Phase 4 — RLS Rollout Checklist
Pre-requisites
- All tenant-scoped models use
TenantManager(Phase 4.1 ✅) SubdomainTenantMiddlewaresetsapp.tenant_idsession var- DB roles created via
setup_rls_roles
Rollout Steps (per environment)
# 1. Dry-run — verify SQL, no changes
python manage.py enable_rls --dry-run
# 2. Apply RLS policies
python manage.py enable_rls
# 3. Verify no query breakage (run test suite)
python -m pytest tests/ -x
# 4. Switch app to non-owner DB user
# Update DATABASE_URL in .env.prod:
# OLD: postgresql://risk_hub_admin:xxx@db/risk_hub
# NEW: postgresql://risk_hub_app:xxx@db/risk_hub
# Keep admin user for migrate service only.
# 5. Restart app containers
docker compose restart web celery
# 6. Smoke test — verify tenant isolation
curl -H "X-Tenant-ID: <uuid>" https://app/api/v1/...
Covered Tables (28 models)
| App | Models |
|---|---|
| risk | Assessment, Hazard |
| actions | ActionItem |
| documents | Document, DocumentVersion |
| approvals | ApprovalWorkflow, ApprovalRequest |
| notifications | Notification, NotificationPreference |
| permissions | Role, Scope, Assignment |
| identity | ApiKey |
| tenancy | Membership, Site |
| explosionsschutz | Area, ExplosionConcept, ZoneDefinition, ProtectionMeasure, Equipment, Inspection, ZoneIgnitionSourceAssessment, VerificationDocument, ZoneCalculationResult, EquipmentATEXCheck |
| substances | Party + all TenantScopedModel subclasses |
| gbu | HazardAssessmentActivity, ActivityMeasure |
Excluded (by design)
Organization— tenant entity itself, not tenant-scopedUser— nullable tenant_id, uses Django's UserManagerPermission,RolePermission,ApprovalStep,ApprovalDecision— no tenant_idTenantScopedMasterDatasubclasses (explosionsschutz) — nullable tenant_id, hybrid isolation
Critical Rule
Organization.id != Organization.tenant_id — always use org.tenant_id for data isolation.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
iil_django_tenancy-0.1.0.tar.gz
(25.9 kB
view details)
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file iil_django_tenancy-0.1.0.tar.gz.
File metadata
- Download URL: iil_django_tenancy-0.1.0.tar.gz
- Upload date:
- Size: 25.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6a2358ae9df559fc382b102707f84fe690f69a7ab55c59aa25d6c16703d80366
|
|
| MD5 |
7fc5514f60e1b72691bcd964a36b63e6
|
|
| BLAKE2b-256 |
e1f383c8242eec1ee8f206ee10faa799bc829c0eed8d14ef1653cea7eaf48a6b
|
File details
Details for the file iil_django_tenancy-0.1.0-py3-none-any.whl.
File metadata
- Download URL: iil_django_tenancy-0.1.0-py3-none-any.whl
- Upload date:
- Size: 28.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
63f0745abda8f8eb0a1ae6e7c37064d5bb05773ef1f59d2120957f7ef84c76a7
|
|
| MD5 |
b6fad7b7d80476cefb52fee10d9584f5
|
|
| BLAKE2b-256 |
d104700ca313927e740f90db8f6c600e13f382e962894f2caeb35eaba502a29e
|