ICVOSS DJANGO PACKAGE REGISTRY

The package index django-boundary Settings Reference

Settings Reference

Documentation

django-boundary is configured entirely through Django's settings.py. All settings use the BOUNDARY_ prefix and are read lazily at access time, so they can be overridden in test suites without restart. The only required setting is BOUNDARY_TENANT_MODEL; everything else has a default.

Required. BOUNDARY_TENANT_MODEL must be set before the application starts. The system check boundary.E001 raises at startup if it is missing or refers to an uninstalled model.


Core

BOUNDARY_TENANT_MODEL

Type str
Default No default -- required

Dotted app_label.ModelName path to the concrete model that represents a tenant. Resolved lazily via django.apps.apps.get_model(), equivalent to how AUTH_USER_MODEL works.

When to change it: Set this once, on project initialisation, to point at whichever model plays the tenant role in your domain -- "tenants.Organisation", "sellers.Merchant", "accounts.Workspace", and so on.

System check: boundary.E001 (Error) fires if this is absent or the model cannot be found.


BOUNDARY_TENANT_FK_FIELD

Type str
Default "tenant"

The default FK field name used by make_tenant_mixin() when no explicit name is passed. Has no direct effect on TenantMixin itself (which always uses "tenant"), but changing this setting means a plain make_tenant_mixin() call without arguments will use the new name.

When to change it: When your domain language is not "tenant" -- for example, "merchant", "organisation", or "workspace". Setting this once propagates the name through BOUNDARY_TENANT_LABEL and BOUNDARY_REQUEST_ATTR automatically (see below).

Interactions: BOUNDARY_TENANT_LABEL defaults to this value. BOUNDARY_REQUEST_ATTR defaults to this value. Changing only this setting is usually sufficient to rename the concept across the whole package.


Terminology

BOUNDARY_TENANT_LABEL

Type str
Default BOUNDARY_TENANT_FK_FIELD (evaluated at access time)

Human-readable term used in error messages, FK verbose_name values, and middleware HTTP response bodies. Defaults to whatever BOUNDARY_TENANT_FK_FIELD is set to, so a project that sets BOUNDARY_TENANT_FK_FIELD = "merchant" automatically gets "merchant" in all user-facing strings without a second setting.

When to change it independently: When the FK column name and the UI copy should differ. For example, the column might be "seller_account" but you want error messages to say "shop".

BOUNDARY_TENANT_FK_FIELD = "seller_account"  # FK column name
BOUNDARY_TENANT_LABEL = "shop"               # used in "No shop is active in context."

BOUNDARY_REQUEST_ATTR

Type str
Default BOUNDARY_TENANT_FK_FIELD (evaluated at access time)

The additional attribute set on the request object by TenantMiddleware. request.tenant is always set for backwards compatibility; when this setting differs from "tenant", the same value is also assigned to request.<REQUEST_ATTR>, so views can use request.merchant or request.workspace instead.

When to change it: When you want views to read request.merchant rather than request.tenant, without breaking any code that still uses request.tenant. Setting BOUNDARY_TENANT_FK_FIELD = "merchant" is usually enough, since this defaults to that value.

Trade-off: If you set this to "tenant", no second attribute is added (no duplication). Any other value means both request.tenant and request.<value> are present and point to the same object.


Safety

BOUNDARY_STRICT_MODE

Type bool
Default True

When True, any queryset evaluated against a TenantModel without an active tenant context raises TenantNotSetError. This is the primary development-time safety net -- it makes accidental cross-tenant data exposure a hard error rather than a silent data leak.

When to disable it: Rarely. The most common reason is during a migration away from a non-boundary codebase where some code paths do not yet carry tenant context. Disable temporarily, fix the gaps, then re-enable.

Trade-off: Disabling strict mode means unscoped queries return all rows filtered only by the ORM -- if the ORM layer is the only enforcement in place (i.e. RLS is not enabled), this risks data leakage between tenants.

System check: boundary.W001 (Warning) fires if this is False.


BOUNDARY_REQUIRED

Type bool
Default True

When True, TenantMiddleware returns a 404 response if no configured resolver can identify a tenant for the request. When False, requests that do not match any resolver proceed without a tenant context set.

When to change it: Set to False for applications that have a mix of tenant-scoped and public (unauthenticated or platform-wide) URLs -- for example, a marketing landing page or a health-check endpoint that must respond without a tenant.

Trade-off: Setting this to False means views must be written defensively -- any view that assumes a tenant context is active will fail silently or raise TenantNotSetError (if strict mode is on) when called from a context-free request.


Resolution

BOUNDARY_RESOLVERS

Type list[str]
Default ["boundary.resolvers.SubdomainResolver"]

Ordered list of dotted-path resolver class names. TenantMiddleware tries each resolver in order and uses the first non-None result. The built-in resolvers are:

Class Resolves from Configured by
boundary.resolvers.SubdomainResolver Subdomain slug (e.g. acme.app.com) BOUNDARY_SUBDOMAIN_FIELD
boundary.resolvers.HeaderResolver HTTP header value BOUNDARY_HEADER_NAME
boundary.resolvers.JWTClaimResolver JWT payload claim BOUNDARY_JWT_CLAIM
boundary.resolvers.SessionResolver Django session key BOUNDARY_SESSION_KEY
boundary.resolvers.ExplicitResolver request.boundary_tenant set upstream none

When to change it: Change the list to match your URL and auth strategy. For public-facing SaaS, SubdomainResolver first is the right choice. For internal APIs backed by a JWT auth middleware, JWTClaimResolver alone is usually sufficient.

Security note: Resolver order determines precedence. Placing HeaderResolver first allows any HTTP client to set the tenant by sending a header. For public-facing applications keep HeaderResolver last or omit it.

System check: boundary.E003 (Error) fires for any class path in this list that cannot be imported.


BOUNDARY_SUBDOMAIN_FIELD

Type str
Default "slug"

The field on the tenant model that SubdomainResolver uses for the lookup. The subdomain extracted from the hostname is matched against this field.

When to change it: If your tenant model uses a field other than slug to identify tenants by subdomain -- for example, "domain" or "short_code".

Interactions: Only used by SubdomainResolver. Has no effect if that resolver is not in BOUNDARY_RESOLVERS.


BOUNDARY_HEADER_NAME

Type str
Default "X-Tenant-ID"

The HTTP header that HeaderResolver reads. The header value is interpreted as a UUID first; if that fails, it falls back to a slug lookup.

When to change it: When your API gateway or proxy injects the tenant identity under a different header name -- for example, "X-Organisation-ID" or "X-Workspace".

Interactions: Only used by HeaderResolver.


BOUNDARY_JWT_CLAIM

Type str
Default "tenant_id"

The claim name within the decoded JWT payload that JWTClaimResolver reads to identify the tenant. Boundary reads this claim only -- it does not validate JWT signatures. Signature validation is the responsibility of your auth middleware.

When to change it: When your identity provider uses a non-standard claim name -- for example, "org_id", "account_uuid", or "tid".

Interactions: Only used by JWTClaimResolver.


BOUNDARY_SESSION_KEY

Type str
Default "boundary_tenant_id"

The Django session key that SessionResolver reads to find the tenant identifier. Useful for internal tools where a user selects their active tenant from a dropdown and that selection is stored in the session.

When to change it: If you already have a session key storing the tenant identity under a different name and want to reuse it rather than duplicate the value.

Interactions: Only used by SessionResolver. Requires Django's session middleware to be active.


BOUNDARY_RESOLVER_CACHE_SIZE

Type int
Default 1000

Maximum number of entries in the process-local LRU cache used by resolvers that perform database lookups. When the cache is full, the least-recently-used entry is evicted.

When to change it: Increase for deployments with many active tenants and a desire to reduce database round-trips on every request. Decrease to reduce memory footprint on constrained instances.

Trade-off: The cache is process-local, so multi-process deployments (gunicorn workers) each maintain their own cache independently. Cache entries are invalidated by TTL and by Django signals on tenant save/delete.

Interactions: Works alongside BOUNDARY_RESOLVER_CACHE_TTL.


BOUNDARY_RESOLVER_CACHE_TTL

Type int
Default 60

Time-to-live in seconds for resolver cache entries. After this period, the next request triggers a fresh database lookup for that tenant key.

When to change it: Lower for deployments where tenant metadata (slug, active status) changes frequently and you need near-real-time invalidation beyond signal-based eviction. Raise to reduce DB load in stable, high-traffic deployments.

Trade-off: A long TTL means a tenant deactivated in the database will continue to resolve successfully until the cache entry expires or the tenant is saved (which triggers signal-based eviction). Signal-based eviction fires within the same process only.


Database and RLS

BOUNDARY_DB_SESSION_VAR

Type str
Default "app.current_tenant_id"

The PostgreSQL session-level variable name set by TenantContext via SET LOCAL. This variable is read by the RLS policy generated by CreateTenantPolicy to enforce row-level isolation at the database layer.

When to change it: If app.current_tenant_id conflicts with another library or in-house convention. The new name must match whatever name your RLS policies reference -- if you change this after generating migrations, regenerate the RLS policies.

Interactions: Directly tied to the RLS policy function. Changing this without updating existing RLS policies will break database-level enforcement. The admin bypass flag is stored separately under BOUNDARY_ADMIN_FLAG_VAR.


BOUNDARY_ADMIN_FLAG_VAR

Type str
Default "app.boundary_admin"

The PostgreSQL session-level variable used by the admin bypass RLS policy. When this variable is set to "true" in a database session, the RLS bypass policy grants full table access -- used by management commands such as boundary_deprovision and boundary_run_all.

When to change it: Only if app.boundary_admin conflicts with another variable in use. Keep this setting consistent with the RLS policy definition; changing it without regenerating policies will silently break management command access.

Interactions: Paired with BOUNDARY_DB_SESSION_VAR. Both are set within the same SET LOCAL block.


BOUNDARY_WRAP_ATOMIC

Type bool
Default True

When True, TenantMiddleware wraps each request in transaction.atomic(). This ensures that SET LOCAL session variables (which are transaction-scoped in PostgreSQL) remain active for the full request duration and are automatically cleared on transaction close.

When to change it: Set to False only if you manage transactions explicitly at the view level and have confirmed that your RLS session variables are still correctly scoped. This is an advanced configuration; the default is safe for virtually all cases.

Trade-off: Disabling this means SET LOCAL variables may not persist as expected if a transaction boundary is crossed mid-request, potentially causing RLS policy violations or TenantNotSetError in subsequent queries within the same request.


Regional

BOUNDARY_REGIONS

Type dict[str, dict] \| None
Default None

A dictionary mapping region key strings to Django database configuration dictionaries (the same format as entries in DATABASES). When set, activates the RegionalRouter, which routes queries for tenant-scoped models to the database associated with the tenant's region.

Non-tenant models (Django internals, sessions, auth) always route to default.

When to change it: Set when your deployment must store tenant data in geographically distinct databases for data residency compliance (GDPR, NHS, etc.).

Trade-off: Regional routing adds operational complexity -- each region needs its own migrations run, its own connection pool, and its own backup strategy. Cross-region joins are not possible via the ORM.

Interactions: Requires "boundary.routing.RegionalRouter" in DATABASE_ROUTERS. System check boundary.E005 (Error) fires if this is set but RegionalRouter is absent from DATABASE_ROUTERS. The region key is read from the field named by BOUNDARY_REGION_FIELD on the tenant instance.


BOUNDARY_REGION_FIELD

Type str
Default "region"

The field on the tenant model that stores the region key. The value must match a key in BOUNDARY_REGIONS. AbstractTenant includes a region field (CharField(50), blank allowed) for this purpose.

When to change it: If your tenant model stores the region under a different field name.

Interactions: Only meaningful when BOUNDARY_REGIONS is set. If the tenant's field value does not match any key in BOUNDARY_REGIONS, the router falls back to default.


Caching

See BOUNDARY_RESOLVER_CACHE_SIZE and BOUNDARY_RESOLVER_CACHE_TTL in the Resolution section above.


Lifecycle Hooks

BOUNDARY_POST_PROVISION_HOOK

Type str \| None
Default None

Dotted-path string to a callable invoked after boundary_provision successfully creates a new tenant. The callable receives the newly created tenant instance as its sole argument.

When to use it: To trigger post-provisioning steps that do not belong in a Django signal -- for example, sending a welcome email, creating default data, or calling an external billing API.

Format:

BOUNDARY_POST_PROVISION_HOOK = "myapp.provisioning.on_tenant_created"

# myapp/provisioning.py
def on_tenant_created(tenant):
    send_welcome_email(tenant.contact_email)

Trade-off: Errors raised inside the hook propagate and will abort the provision command. Wrap with try/except if the hook should be non-fatal.


BOUNDARY_PRE_DEPROVISION_HOOK

Type str \| None
Default None

Dotted-path string to a callable invoked before boundary_deprovision deletes a tenant. The callable receives the tenant instance about to be deleted. Raising an exception from this hook aborts the deprovision operation.

When to use it: To run pre-deletion checks or cleanup that must complete before data is destroyed -- for example, cancelling active subscriptions, notifying users, or finalising billing.

Format:

BOUNDARY_PRE_DEPROVISION_HOOK = "myapp.provisioning.before_tenant_deleted"

# myapp/provisioning.py
def before_tenant_deleted(tenant):
    cancel_subscription(tenant.stripe_subscription_id)

Trade-off: Because this hook runs before the NDJSON export and deletion, a hook failure with --yes specified will abort entirely with no data loss. Use --dry-run to verify the hook would succeed before running destructively.


Quick Reference

Setting Default Required
BOUNDARY_TENANT_MODEL -- Yes
BOUNDARY_TENANT_FK_FIELD "tenant" No
BOUNDARY_TENANT_LABEL BOUNDARY_TENANT_FK_FIELD No
BOUNDARY_REQUEST_ATTR BOUNDARY_TENANT_FK_FIELD No
BOUNDARY_STRICT_MODE True No
BOUNDARY_REQUIRED True No
BOUNDARY_RESOLVERS ["boundary.resolvers.SubdomainResolver"] No
BOUNDARY_SUBDOMAIN_FIELD "slug" No
BOUNDARY_HEADER_NAME "X-Tenant-ID" No
BOUNDARY_JWT_CLAIM "tenant_id" No
BOUNDARY_SESSION_KEY "boundary_tenant_id" No
BOUNDARY_RESOLVER_CACHE_SIZE 1000 No
BOUNDARY_RESOLVER_CACHE_TTL 60 No
BOUNDARY_DB_SESSION_VAR "app.current_tenant_id" No
BOUNDARY_ADMIN_FLAG_VAR "app.boundary_admin" No
BOUNDARY_WRAP_ATOMIC True No
BOUNDARY_REGIONS None No
BOUNDARY_REGION_FIELD "region" No
BOUNDARY_POST_PROVISION_HOOK None No
BOUNDARY_PRE_DEPROVISION_HOOK None No