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.2.2 to 1.3.0¶
A drop-in upgrade -- no code changes are required and runtime behaviour is unchanged. The release is additive, with one optional, opt-in performance step.
Optional: adopt the InitPlan policy form (#57)¶
v1.3.0 wraps every GUC read in a policy predicate in an uncorrelated scalar
sub-SELECT -- (SELECT current_setting('rls.current_tenant', true)) -- so
PostgreSQL evaluates the variable once per statement (a planner InitPlan)
instead of once per row. The predicate is otherwise identical, so query results
never change; the benefit shows on large, admin, and raw-SQL scans.
Because RLSConstraint / RLSM2MConstraint serialize identically
(deconstruct() is unchanged), Django generates no new migration, and the
CREATE POLICY ... IF NOT EXISTS guard skips tables that already have a
policy. So new deployments get the InitPlan form automatically, while
existing policies keep the previous inline form until you drop and recreate
them. Adopting it on a running database is optional and can be done
table-by-table.
M2M through tables -- drop the policy, then re-run setup_m2m_rls, which
recreates a policy only on tables that don't have one:
python manage.py setup_m2m_rls # recreates the dropped policy (InitPlan form)
python manage.py check_rls --verbose # confirm the live USING / WITH CHECK
Standard tenant tables -- recreate the policy through the migration that
defines the RLSConstraint. Reversing that migration runs the constraint's
remove_sql (drops the policy and disables RLS); re-applying runs create_sql,
which now emits the v1.3.0 InitPlan SQL:
python manage.py migrate <app> <migration_before_the_RLSConstraint> # drops the old policy
python manage.py migrate <app> <migration_that_adds_the_RLSConstraint> # recreates it
Warning
Reversing the migration briefly disables RLS on that table. Run it inside
a maintenance window, or instead drop and recreate the single policy with
explicit SQL in one transaction. Always confirm with
python manage.py check_rls --verbose, which prints the live USING /
WITH CHECK so you can verify the (SELECT current_setting(...)) form is in
place.
Policy names follow "{table}_tenant_isolation_policy" (standard models) and
"{through_table}_m2m_rls_policy" (M2M join tables).
From 1.2.1 to 1.2.2¶
A drop-in upgrade -- no breaking changes, and no code or data migration is required. The release is purely additive:
- New optional CLI flags:
check_rls --verboseandsetup_m2m_rls --verboseprint each policy's SQL for auditing. Existing invocations behave exactly as before. - New system checks
W008andW009:python manage.py checknow warns whenRLS_TENANTS['TENANT_MODEL']does not resolve to an installed model (W008) or when anRLSProtectedModel's RLS-policy tenant field is missing (W009). These are warnings, not errors -- you may see new output if your configuration has a latent issue, but nothing breaks. - New Celery Tasks guide documenting how to set RLS context in background tasks.
From 1.2.0 to 1.2.1¶
This release has one breaking change: internal helpers have been removed from the top-level package exports.
What Changed¶
-
Removed from top-level exports: Raw state functions (
get_current_tenant_id,set_current_tenant_id,reset_current_tenant_id,get_rls_context_active,set_rls_context_active,reset_rls_context_active) and exception classes (NoTenantContextError,RLSConfigurationError,RLSTenantError) are no longer in__all__or importable viafrom django_rls_tenants import .... -
Still importable from submodules: All removed symbols remain available from their actual modules.
Upgrade Steps¶
-
Update imports that reference the removed symbols from the top-level package:
-
No other changes required. The context managers (
tenant_context,admin_context,with_rls_context) remain top-level exports and are the recommended API for managing RLS state.
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):
# Before (1.1.0) from django_rls_tenants import tenant_context try: with tenant_context(tenant_id=None): ... except ValueError: ... # After (1.2.0) from django_rls_tenants import tenant_context from django_rls_tenants.exceptions import NoTenantContextError try: with tenant_context(tenant_id=None): ... except NoTenantContextError: ... -
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.