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.
Metadata
Release files for iil-django-tenancy 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| iil_django_tenancy-0.1.0.tar.gz | 25.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| iil_django_tenancy-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 54.5 kB
Release files / iil_django_tenancy-0.1.0.tar.gz
| Download URL | iil_django_tenancy-0.1.0.tar.gz |
|---|---|
| Size | 25.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6a2358ae9df559fc382b102707f84fe690f69a7ab55c59aa25d6c16703d80366
|
|
BLAKE2b-256 checksum How to use checksums |
e1f383c8242eec1ee8f206ee10faa799bc829c0eed8d14ef1653cea7eaf48a6b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.12
|
Release files / iil_django_tenancy-0.1.0-py3-none-any.whl
| Download URL | iil_django_tenancy-0.1.0-py3-none-any.whl |
|---|---|
| Size | 28.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
63f0745abda8f8eb0a1ae6e7c37064d5bb05773ef1f59d2120957f7ef84c76a7
|
|
BLAKE2b-256 checksum How to use checksums |
d104700ca313927e740f90db8f6c600e13f382e962894f2caeb35eaba502a29e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.12
|
Security for iil-django-tenancy 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.