Run cross-tenant admin operations
Goal
Operate across every tenant at once, or against a tenant other than the one in the current context. This covers the deliberate escape hatches that boundary provides: the unscoped manager, the all_regions() context manager, the boundary_run and boundary_run_all management commands, and the row-level security (RLS) admin bypass flag.
These are sharp tools. By default boundary filters every query to the active tenant so a missing context is fail-closed. The escape hatches turn that protection off on purpose. Read the safety notes in each section before using them in production code.
Prerequisites
- boundary is installed and
BOUNDARY_TENANT_MODELis set. See the README for setup. - Your models inherit from
TenantMixin,TenantModel, or a mixin produced bymake_tenant_mixin(), so they expose bothobjects(tenant-filtered) andunscoped(unfiltered) managers. - For the regional sections,
BOUNDARY_REGIONSis configured andboundary.routing.RegionalRouteris inDATABASE_ROUTERS. - For the RLS bypass section, you have applied the RLS policies via the migration operations. See the README RLS section for how policies are created.
Steps
1. Read or write across all tenants with the unscoped manager
Every tenant-scoped model has an unscoped manager alongside the default objects manager. unscoped is a plain manager that does not apply the active-tenant filter, so it returns rows for all tenants regardless of context.
from myapp.models import Booking
# Default manager: filtered to the active tenant (or fail-closed if none).
Booking.objects.count()
# Unscoped manager: every row, every tenant.
Booking.unscoped.count()
Booking.unscoped.filter(court=1)
total = Booking.unscoped.aggregate(total=Sum("price"))
Use this for platform-level reporting, analytics, and admin dashboards where you genuinely need to see across tenants.
When you create rows through unscoped, boundary does not auto-populate the tenant field from context. You must pass the tenant explicitly:
# Correct: explicit tenant.
Booking.unscoped.create(court=1, tenant=some_tenant)
# Wrong: no tenant, no auto-populate. Raises IntegrityError on a non-null FK.
Booking.unscoped.create(court=1)
unscoped.bulk_create() behaves the same way: it skips auto-populate, so each object must already carry its tenant.
Safety: a query on
unscopedis a query with no isolation. Treat any code path that reachesunscopedas privileged. Keep it out of request handlers that serve tenant users, and prefer the tenant-filteredobjectsmanager everywhere else.
2. Iterate across regional databases with all_regions()
In a multi-region deployment, tenant data is sharded across database aliases by region. A single unscoped query only hits the database it is routed to, so to aggregate across regions you must query each alias.
all_regions() yields the configured region aliases (or ["default"] when BOUNDARY_REGIONS is unset), so you can loop over them with .using():
from boundary.routing import all_regions
from myapp.models import Booking
grand_total = 0
with all_regions() as aliases:
for alias in aliases:
grand_total += Booking.unscoped.using(alias).count()
To pin queries to one specific region instead of routing by the active tenant, use specific_region():
from boundary.routing import specific_region
with specific_region("eu-west"):
bookings = Booking.unscoped.all() # hits the eu-west database
An unknown region key in specific_region() falls back to the default alias rather than raising.
Safety: combine
all_regions()withunscopedonly for cross-tenant aggregation. If you forget.using(alias), you silently aggregate just one region and under-report.
3. Run a command for one specific tenant with boundary_run
boundary_run activates a tenant context, then calls another management command inside it. Use it to run a per-tenant job against a single tenant from the shell.
python manage.py boundary_run --tenant club-a send_reminders --dry-run
--tenantis required. It accepts the tenant PK or slug. boundary tries PK first, then slug, and raisesCommandErrorif neither matches.- Everything after the inner command name is forwarded verbatim to that command (
--dry-runabove goes tosend_reminders).
The inner command runs inside TenantContext.using(tenant), so any boundary-aware code it executes is correctly scoped to that one tenant.
4. Run a command for every active tenant with boundary_run_all
boundary_run_all resolves every active tenant (is_active=True), then runs the inner command once per tenant, each inside its own tenant context.
# Sequentially, against all active tenants.
python manage.py boundary_run_all send_reminders
# 4 parallel workers, only EU tenants, machine-readable output.
python manage.py boundary_run_all send_reminders --parallel 4 --region eu-west --json
Options:
--parallel N: number of concurrent workers (default1). Values above1run each tenant in a separate process via a multiprocessing pool.--region REGION: limit to tenants whose region field matchesREGION. The field name comes fromBOUNDARY_REGION_FIELD(defaultregion).--exclude PK: skip a tenant by PK. Repeat the flag to exclude several:--exclude 7 --exclude 9.--json: emit one NDJSON object per tenant for piping into other tools.
Output is one line per tenant. In human mode, successes print [OK] <slug> to stdout and failures print [FAIL] <slug>: <error> to stderr. In --json mode each line is an object with tenant and status keys (and error on failure):
{"tenant": "club-a", "status": "ok"}
{"tenant": "club-b", "status": "error", "error": "..."}
A failure in one tenant does not abort the run. Each tenant is isolated, so the loop continues and the failure is reported per tenant.
Safety:
boundary_run_alltouches every active tenant. Test the inner command withboundary_run --tenant <one>first, then widen toboundary_run_all. When using--parallel, the inner command must be safe to run in multiple processes at once.
5. Bypass RLS for trusted maintenance work
When PostgreSQL row-level security is enabled, the database itself rejects rows outside the active tenant, even for the unscoped manager, unless the connection is a superuser or the admin bypass flag is set. boundary installs an admin bypass policy that lifts isolation, for both reads AND writes, when the app.boundary_admin session variable is 'true': the policy has no WITH CHECK, so PostgreSQL falls back to its USING clause for write checks too, and multiple permissive policies are combined with OR, so satisfying this policy alone is enough regardless of the tenant isolation policy's own WITH CHECK. An INSERT or UPDATE for a tenant other than the active one is accepted, not just made visible.
Use boundary.context.admin_bypass() to set the flag. It is the only supported way to reach it:
from boundary.context import admin_bypass
from myapp.models import Booking
with admin_bypass():
# Queries in this block see and write rows for all tenants, even with
# FORCE ROW LEVEL SECURITY on the table.
Booking.unscoped.filter(court=1).update(is_paid=True)
admin_bypass() hardcodes the transaction-local form of set_config (the third argument is always true): a consumer cannot reach the session-scoped form through this API, because the session-scoped form is unsafe under connection reuse (see below) and the whole point of the function is to remove that failure mode rather than merely warn about it. It guarantees an active transaction (opening one automatically unless BOUNDARY_WRAP_ATOMIC=False, matching TenantContext.using()), verifies the flag actually took effect before running your code, and clears it explicitly on exit rather than relying only on the transaction ending. It also fires boundary.signals.admin_bypass_activated on entry, so every use of this escape hatch is observable. See the README signals reference and the docstring on admin_bypass() for the full contract, including nested use and multi-region operations.
The variable name is configurable via BOUNDARY_ADMIN_FLAG_VAR (default app.boundary_admin); admin_bypass() always reads it from settings rather than a hardcoded string, so a customised name is honoured automatically.
If you must set the flag with a hand-written set_config call (there is no supported reason to, but if you are debugging boundary itself or writing a migration operation), the third argument MUST be true, never false or omitted. The difference is not cosmetic:
- Transaction-local (
true, whatadmin_bypass()always uses): scoped to the current transaction. It clears automatically on commit or rollback, and it cannot survive a connection being returned to a pool or reused for the next request underCONN_MAX_AGE, because the transaction that held it is already gone by the time that happens. - Session-scoped (
false, never reachable throughadmin_bypass()): persists on the connection until something clears it, across every subsequent statement and transaction on that connection. Under Django'sCONN_MAX_AGE(a connection reused across multiple requests) or behind an external pooler such as PgBouncer (a connection handed to a completely different, unrelated request), a session-scoped flag set for one piece of maintenance work silently stays active for whatever request or task picks up that connection next, with no isolation at all and no indication why.DISCARD ALL, which some poolers run on connection handback, resets session-levelset_configstate, but relying on pooler configuration to clean up after an unsafe API is a second point of failure, not a fix. This is the exact failure modeadmin_bypass()exists to make unreachable.
Safety: the admin flag disables the database's last line of defence. Use
admin_bypass()for trusted maintenance only, never on a connection that serves tenant traffic, and never assume it is still active outside thewithblock:admin_bypass()clears it explicitly, but a hand-rolled session-scopedset_configwould not.
6. Bootstrap writes to an adopted table need a tenant context, not admin_bypass()
If you have adopted a third-party app (BOUNDARY_TENANT_APPS plus the AdoptTenantApp migration operation), its tables carry a tenant_id column declared NOT NULL DEFAULT boundary_current_tenant_id(). Any write to one of those tables with no active tenant fails closed: the default evaluates to NULL, the NOT NULL constraint rejects the row, and the caller sees an IntegrityError. That is correct behaviour, and it must not be softened by making the column nullable or relaxing the isolation policy.
admin_bypass() is not the remedy for that failure, and TenantContext.using() is. The failure is a NOT NULL constraint rejecting a NULL the column default produced, and a constraint is evaluated before any policy is consulted. The bypass flag only changes which rows the boundary_admin_bypass policy admits; it has no bearing on what boundary_current_tenant_id() returns. An ORM write inside admin_bypass() with no tenant active still raises IntegrityError.
from boundary.context import TenantContext
# Wrong: the flag does not give the column default a tenant to stamp.
with admin_bypass():
EmailAddress.objects.create(user=user, email="a@example.com") # IntegrityError
# Right: the session variable is set, the default stamps that tenant, and
# the isolation policy's WITH CHECK is satisfied with no bypass at all.
with TenantContext.using(tenant):
EmailAddress.objects.create(user=user, email="a@example.com")
These are the paths that run with no tenant, and therefore need attention once an app is adopted:
| Path | Why it runs without a tenant | Remedy |
|---|---|---|
createsuperuser |
An interactive management command, no request and no resolved tenant | Run it inside TenantContext.using(tenant), or wrap it, if auth is adopted |
loaddata |
Fixtures load outside any request cycle | Run it inside TenantContext.using(tenant) for the tenant the fixture belongs to |
post_migrate handlers |
Run at the end of migrate, before any request or resolver, with no call site to wrap |
None |
| An adopted app's own data migrations | Run inside migrate, in the same no-tenant state |
None |
The last two have no clean remedy. An adopted app whose data migration inserts seed rows cannot be adopted as it stands: exclude that model via BOUNDARY_ADOPT_EXCLUDE, leaving its table global, or seed the rows per tenant from your own code inside TenantContext.using(tenant). Django's own two shipped post_migrate handlers write only auth.Permission and contenttypes.ContentType, both refused by adoption, so neither is affected unless you adopt auth itself.
admin_bypass() stays the right tool for exactly two cases on an adopted table: a cross-tenant read, and raw SQL that supplies tenant_id explicitly, which is how boundary_deprovision reaches these tables.
See Adopt a third-party app into your tenancy for the full adoption procedure.
Verify it worked
unscoped: with two tenants each owning one row, assertModel.unscoped.count() == 2while a single tenant is active, andModel.objects.count() == 1.all_regions(): withBOUNDARY_REGIONSset to three regions, confirm the yielded aliases match the configured keys; with it unset, confirm you get["default"].boundary_run: run it with a harmless inner command such aspython manage.py boundary_run --tenant <slug> showmigrations --listand confirm noCommandError.boundary_run_all: runpython manage.py boundary_run_all showmigrations --jsonand confirm one JSON line per active tenant, each withstatus: ok.- RLS bypass: with isolation applied and rows for two tenants, enter
admin_bypass()and confirm a rawSELECT count(*)returns the full count instead of the per-tenant count, and thatcurrent_setting('app.boundary_admin', true)reads''again immediately after the block exits.
Common pitfalls
- Calling
unscoped.create()orunscoped.bulk_create()without setting the tenant explicitly: auto-populate is skipped, so a non-null FK raisesIntegrityError. Always passtenant=.... - Expecting
unscopedto cross regions: it only queries the database it routes to. Wrap it inall_regions()and use.using(alias)to span shards. - Reaching for
unscopedin tenant-facing request code: this defeats isolation. Use the defaultobjectsmanager and a properTenantContextinstead. See How tenant resolution works. - Forgetting that
boundary_run_allonly targetsis_active=Truetenants. Inactive tenants are skipped silently. - Passing inner-command flags before the inner command name in
boundary_run. The inner command name comes first, then its arguments. - Setting
app.boundary_adminby hand instead of viaadmin_bypass(). A hand-writtenset_configcall with the third argumentfalse(or aSETstatement, which is always session-scoped) leaves the flag active on the connection indefinitely, and underCONN_MAX_AGEor an external pooler that connection is handed to a later, unrelated request.admin_bypass()makes this failure mode unreachable by hardcoding the transaction-local form; there is no supported way to opt into the session-scoped form through it. - Reaching for
admin_bypass()when a write to an adopted table raisesIntegrityErrorontenant_id. The flag cannot help: theNOT NULLconstraint is evaluated before any policy, and the flag does not change what the column default produces. UseTenantContext.using(tenant)instead (step 6). - Assuming
admin_bypass()only widens what is visible. It also lifts the write check (noWITH CHECKon the bypass policy, permissive policies OR together), so code inside the block can INSERT or UPDATE rows for any tenant, not only read them.
Related
- README for the full settings reference, RLS setup, and the regional routing model.
- README sections on the
unscopedmanager,all_regions/specific_region, theboundary_run/boundary_run_allcommands, andadmin_bypass().