Migration Guide¶
This guide helps you migrate from other Django multitenancy libraries to django-rls-tenants.
From django-tenants¶
django-tenants uses a schema-per-tenant approach. Migrating to django-rls-tenants means moving from separate schemas to a single schema with RLS policies.
Key Differences¶
| Aspect | django-tenants | django-rls-tenants |
|---|---|---|
| Isolation | Separate PostgreSQL schemas | RLS policies on shared tables |
| Tenant routing | connection.set_tenant() |
GUC variables via middleware |
| Shared apps | SHARED_APPS / TENANT_APPS |
All apps share one schema |
| Migrations | Run per-schema | Run once (single schema) |
| Raw SQL safety | Yes (schema isolation) | Yes (RLS policies) |
| Database overhead | High (N schemas) | Low (single schema) |
Migration Steps¶
-
Create a tenant FK column on all tenant-scoped tables:
-
Replace model inheritance: change
TenantMixinto your own tenant model, and tenant-scoped models to inherit fromRLSProtectedModel. -
Replace middleware: swap
TenantMainMiddlewareforRLSTenantMiddleware. -
Replace tenant routing: replace
connection.set_tenant()calls withtenant_context()oradmin_context(). -
Consolidate schemas into a single schema (this is the hardest step and is project-specific).
-
Run migrations to create RLS policies.
-
Verify: run
python manage.py check_rls.
Warning
Schema consolidation is a significant data migration and should be planned carefully. Test thoroughly in a staging environment before production.
From django-multitenant¶
django-multitenant uses ORM-level query rewriting. Migrating is simpler because you already use a single schema.
Key Differences¶
| Aspect | django-multitenant | django-rls-tenants |
|---|---|---|
| Isolation | ORM query rewriting | RLS policies |
| Raw SQL safety | No | Yes |
| Citus support | Yes | No (standard PostgreSQL) |
| Fail-closed | No | Yes |
| Manager | TenantManager |
RLSManager |
Migration Steps¶
-
Replace model base class: change
TenantModeltoRLSProtectedModel. -
Replace manager calls: change
set_current_tenant()to context managers. -
Replace middleware: swap the multitenant middleware for
RLSTenantMiddleware. -
Add
TenantUserproperties to your User model. -
Update settings: replace
MULTI_TENANTsettings withRLS_TENANTS. -
Run migrations to create RLS policies.
-
Verify: run
python manage.py check_rls.
From No Multitenancy¶
If you are adding multitenancy to an existing single-tenant application:
- Create a Tenant model (see Tenant Model).
- Add tenant FK to all data models that need isolation.
- Populate the FK with the appropriate tenant ID for existing data.
- Inherit from
RLSProtectedModelon those models. - Implement
TenantUseron your User model. - Add middleware and settings.
- Run migrations and verify with
check_rls.
The most challenging step is populating the tenant FK for existing data. Plan a data migration that assigns the correct tenant to each existing record.
Upgrading django-rls-tenants¶
From 1.1.0 to 1.2.0¶
This release has one minor breaking change: some ValueError exceptions have
been replaced with custom exception types.
What Changed¶
-
Custom exceptions: The library introduces a custom exception hierarchy in
django_rls_tenants.exceptions.tenant_context(None)and_resolve_user_guc_vars()now raiseNoTenantContextErrorinstead ofValueError.RLSTenantsConfig._get()now raisesRLSConfigurationErrorinstead ofValueError. If you catchValueErrorfrom these functions, update your except clauses. Both are subclasses ofRLSTenantError, which is a subclass ofException. -
Multi-database GUC support: The middleware now sets GUC variables on all database aliases listed in
RLS_TENANTS["DATABASES"](default:["default"]). No changes needed for single-database setups. -
Strict mode (
STRICT_MODE=True): An opt-in setting that raisesNoTenantContextErrorwhen queries execute without an active RLS context. Off by default -- existing behavior is unchanged. -
New public API:
get_rls_context_active(),set_rls_context_active(),reset_rls_context_active()for tracking whether an RLS context is active. These are primarily used internally by strict mode but are available for custom middleware implementations.
Upgrade Steps¶
-
Update the package:
-
Update exception handling (if applicable):
-
Optional: enable multi-database support:
-
Optional: enable strict mode:
-
Verify: run
python manage.py checkandpython manage.py check_rls.
From 1.0.0 to 1.1.0¶
This release has no breaking changes. All existing code continues to work without modification.
What Changed¶
-
RLS policy SQL:
RLSConstraintnow generatesCASE WHENpolicies instead ofOR-based policies, improving readability and clarifying the evaluation structure. (The primary performance improvement comes from auto-scoping below, which enables composite index usage.) -
Automatic query scoping:
RLSManager.get_queryset()now addsWHERE tenant_id = Xautomatically when a tenant context is active (viatenant_context(),admin_context(), orRLSTenantMiddleware). This enables composite indexes and eliminates sequential scan penalties at scale. -
New public API:
get_current_tenant_id(),set_current_tenant_id(), andreset_current_tenant_id()are available for custom middleware and management commands that need direct access to the auto-scope state.
Upgrade Steps¶
-
Update the package:
-
Generate a new migration to update the RLS policy SQL:
This replaces the
OR-based policy with theCASE WHENstructure. The migration is safe to run on a live database -- it drops and recreates the policy in a single DDL statement. -
Verify: run
python manage.py check_rls.
Behavioral Notes¶
for_user()continues to work exactly as before.- Auto-scoping activates automatically -- no code changes required. If both
auto-scoping and
for_user()are active simultaneously, the query gets two redundantWHERE tenant_id = Xclauses. This is by design for defense-in-depth; the cost of the double equality check per row is negligible. TenantQuerySet.select_related()now auto-propagates tenant filters to joined RLS-protected tables when a tenant context is active.