Comparison with Alternatives¶
Overview¶
| Feature | django-rls-tenants | django-tenants | django-multitenant |
|---|---|---|---|
| Isolation level | Database-enforced (RLS policies) | Schema-per-tenant | Application-level (Citus) |
| Raw SQL safety | Filtered by DB automatically | Must use correct schema search_path | Not enforced |
| dbshell safety | Filtered by DB automatically | Must set search_path manually | Not enforced |
| Migration complexity | Single schema, standard migrations | One migration per tenant schema | Single schema, standard migrations |
| Scaling (1000+ tenants) | Single schema, no overhead | One PG schema per tenant (catalog bloat) | Depends on Citus setup |
| PostgreSQL required | Yes | Yes | Yes (Citus extension) |
| Django version support | 4.2 — 6.0 | 4.0+ | 3.2+ |
Schema-Per-Tenant: django-tenants¶
How it works¶
- Each tenant gets a dedicated PostgreSQL schema.
- Django's
search_pathis switched per request via middleware. - Shared tables (e.g., tenants themselves) live in the
publicschema.
Strengths¶
- True schema-level isolation — tenants cannot see each other's tables.
- Per-tenant schema customization is possible (different indexes, columns).
- Mature ecosystem with years of production use.
Weaknesses¶
- Migration time scales linearly: N tenants = N migration runs. At 1000+
tenants, a single
migratecan take hours. - PostgreSQL catalog bloat: each schema adds entries to
pg_class,pg_attribute, etc. At scale this slows down introspection and planning. - Connection pooling complexity:
search_pathmust be set on every connection checkout. PgBouncer transaction mode requires extra care. - Operational overhead: backup/restore, schema cleanup, and monitoring multiply with tenant count.
Application-Level Filtering: django-multitenant¶
How it works¶
- Designed for Citus (distributed PostgreSQL).
- Rewrites ORM queries to inject tenant filters automatically.
- Uses a thread-local or context variable to track the current tenant.
Strengths¶
- Works with Citus distributed tables for horizontal scaling.
- Single schema — no migration overhead per tenant.
- Familiar Django ORM API.
Weaknesses¶
- Raw SQL is not protected: any
cursor.execute()call bypasses the filter. The developer must remember to addWHERE tenant_id = .... - dbshell is not protected:
SELECT * FROM tablereturns all rows. - Management commands and data migrations run without tenant context unless explicitly wrapped.
- Depends on Citus: while it can work without Citus, the primary value proposition is the Citus integration.
Row-Level Security: django-rls-tenants¶
How it works¶
- A PostgreSQL session variable (GUC) is set per request via middleware.
- RLS policies on each protected table filter rows based on that GUC.
- Policies are created automatically via Django migrations — no hand-written SQL.
Strengths¶
- Database-enforced: the filter is in PostgreSQL, not in Python.
Raw SQL,
dbshell, and migrations are all subject to the policy. - Fail-closed: missing tenant context = zero rows, not all rows.
- Single schema: standard Django migrations run once, not once per tenant.
- No catalog bloat: 1000 tenants ≈ same DB footprint as 1 tenant.
Weaknesses¶
- PostgreSQL only: RLS is a PostgreSQL feature. No MySQL/SQLite support.
- No per-tenant schema customization: all tenants share the same table structure.
- Newer ecosystem: less community history compared to
django-tenants.
Decision Guide¶
| Situation | Recommended |
|---|---|
| Need per-tenant schema customization | django-tenants |
| Using Citus for horizontal scaling | django-multitenant |
| Shared schema, need DB-enforced isolation | django-rls-tenants |
| < 5 tenants, simple requirements | django-tenants (simpler mental model) |
| 10–10,000+ tenants, shared schema | django-rls-tenants |
| Must protect raw SQL and dbshell | django-rls-tenants |
| Not using PostgreSQL | None of the above (consider application-level filtering) |