ICVOSS DJANGO PACKAGE REGISTRY

The package index django-boundary Choose and order resolvers

Choose and order resolvers

Documentation

Goal

Pick the right built-in resolver for how your clients identify a tenant, order them correctly with BOUNDARY_RESOLVERS, and add a custom resolver when none of the built-ins fit.

A resolver answers one question per request: which tenant does this request belong to? The middleware walks BOUNDARY_RESOLVERS in order, calls resolve(request) on each, and stops at the first resolver that returns a tenant. First match wins.

Prerequisites

Steps

1. Understand each built-in resolver

All built-ins live in boundary.resolvers and subclass BaseResolver. Each resolve(request) returns a tenant instance or None to pass control to the next resolver. None of them raise on a miss.

Resolver Reads from Lookup Setting
SubdomainResolver First label of the host (club-a.example.com) SUBDOMAIN_FIELD on the tenant model BOUNDARY_SUBDOMAIN_FIELD (default "slug")
HeaderResolver An HTTP header UUID pk, then raw pk, then slug BOUNDARY_HEADER_NAME (default "X-Tenant-ID")
JWTClaimResolver The Authorization: Bearer token payload tenant pk from a claim, signature NOT validated BOUNDARY_JWT_CLAIM (default "tenant_id")
SessionResolver The Django session tenant pk from a session key BOUNDARY_SESSION_KEY (default "boundary_tenant_id")
ExplicitResolver request.boundary_tenant set by upstream code direct attribute read, no DB query none

When to use each:

2. Configure the order

BOUNDARY_RESOLVERS is a list of dotted class paths. Order is precedence: the middleware returns the first non-None result.

# settings.py
BOUNDARY_RESOLVERS = [
    "boundary.resolvers.ExplicitResolver",   # honour explicit overrides first
    "boundary.resolvers.SubdomainResolver",  # then public subdomain
    "boundary.resolvers.SessionResolver",    # then logged-in session choice
]

Order by trust and specificity. Put the most authoritative source first and the broadest fallback last.

3. Set per-resolver options

Each resolver reads its own BOUNDARY_ setting. Override only the ones whose resolver you use.

# settings.py
BOUNDARY_SUBDOMAIN_FIELD = "slug"          # tenant field for subdomain and header-slug lookups
BOUNDARY_HEADER_NAME = "X-Tenant-ID"       # header HeaderResolver reads
BOUNDARY_JWT_CLAIM = "org_id"              # claim JWTClaimResolver reads
BOUNDARY_SESSION_KEY = "boundary_tenant_id"  # session key SessionResolver reads

Note that HeaderResolver's slug fallback uses BOUNDARY_SUBDOMAIN_FIELD, not a separate setting.

The full settings table is in the settings reference.

4. Write a custom resolver

Subclass BaseResolver and implement resolve(self, request). Return a tenant or None. Call self.get_tenant_model() to get the configured tenant model rather than importing it directly, and never let resolve raise: log and return None on error.

# myapp/resolvers.py
import logging

from boundary.resolvers import BaseResolver

logger = logging.getLogger(__name__)


class PathPrefixResolver(BaseResolver):
    """Resolve tenant from a /t/<slug>/ URL prefix."""

    def resolve(self, request):
        parts = request.path.split("/")
        if len(parts) < 3 or parts[1] != "t":
            return None

        slug = parts[2]
        TenantModel = self.get_tenant_model()
        try:
            return TenantModel.objects.get(slug=slug, is_active=True)
        except TenantModel.DoesNotExist:
            return None
        except Exception:
            logger.exception("PathPrefixResolver failed for slug=%s", slug)
            return None

Register it by dotted path:

# settings.py
BOUNDARY_RESOLVERS = [
    "myapp.resolvers.PathPrefixResolver",
    "boundary.resolvers.SubdomainResolver",
]

Verify it worked

Call the resolver directly with a Django RequestFactory, the same way the test suite does.

import pytest
from django.test import RequestFactory

from boundary.resolvers import SubdomainResolver


@pytest.mark.django_db
def test_subdomain_resolution(tenant_a):
    request = RequestFactory().get("/", HTTP_HOST="club-a.example.com")
    assert SubdomainResolver().resolve(request) == tenant_a

To confirm ordering end to end, send a real request through the middleware and read the resolved tenant off the request. After successful resolution the middleware sends the tenant_resolved signal with tenant, resolver, and request; if nothing matches and BOUNDARY_REQUIRED is True it sends tenant_resolution_failed and returns a 404. The resolved tenant is available as request.tenant (and as request.<BOUNDARY_REQUEST_ATTR> when you have customised that).

Common pitfalls