ICVOSS DJANGO PACKAGE REGISTRY

The package index django-boundary Deploy across multiple regions

Deploy across multiple regions

Documentation

Goal

Route each tenant's queries to a geographically distinct database so that tenant data stays in its required region (for example, UK data in a UK database, EU data in an EU database). This is how django-boundary supports data residency requirements.

How regional routing works

Boundary ships a Django database router, RegionalRouter, that inspects the active tenant in TenantContext and returns the database alias that matches the tenant's region.

  1. Each tenant stores a region key in a field (default region).
  2. BOUNDARY_REGIONS maps each region key to a database configuration.
  3. RegionalRouter reads the active tenant's region and routes reads and writes for tenant-scoped models to the matching alias.

The router falls back to the default alias, never raising, when any of the following are true:

This fail-safe behaviour means a misconfigured region degrades to the default database rather than crashing a request.

Prerequisites

Steps

  1. Define one database alias per region in DATABASES. The alias names must match the keys you will use in BOUNDARY_REGIONS.

python # settings.py DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "app_default", "HOST": "default.db.example.com", }, "uk": { "ENGINE": "django.db.backends.postgresql", "NAME": "app_uk", "HOST": "uk.db.example.com", }, "eu-west": { "ENGINE": "django.db.backends.postgresql", "NAME": "app_eu", "HOST": "eu.db.example.com", }, }

  1. Configure BOUNDARY_REGIONS. The keys are the region values stored on your tenants and must match the database aliases above. The values are database configuration dicts (the same shape Django uses in DATABASES).

python # settings.py BOUNDARY_REGIONS = { "uk": {"ENGINE": "django.db.backends.postgresql", "HOST": "uk.db.example.com"}, "eu-west": {"ENGINE": "django.db.backends.postgresql", "HOST": "eu.db.example.com"}, }

  1. Add RegionalRouter to DATABASE_ROUTERS.

python # settings.py DATABASE_ROUTERS = ["boundary.routing.RegionalRouter"]

  1. If your tenant model stores the region under a field other than region, point Boundary at it.

python # settings.py BOUNDARY_REGION_FIELD = "data_region"

  1. Set each tenant's region to a key that exists in BOUNDARY_REGIONS.

python tenant.region = "uk" tenant.save()

Verify it worked

With a tenant active, the router resolves to the tenant's region alias for tenant-scoped models, and to default for everything else.

from boundary.context import TenantContext
from boundary.routing import RegionalRouter
from myapp.models import Patient  # a tenant-scoped model

uk_tenant.region = "uk"
uk_tenant.save()

router = RegionalRouter()

with TenantContext.using(uk_tenant):
    assert router.db_for_read(Patient) == "uk"
    assert router.db_for_write(Patient) == "uk"

    # Writes land in the UK database automatically
    Patient.objects.create(name="Smith")

Non-tenant models, and queries with no active tenant, route to default:

from django.contrib.auth.models import User

with TenantContext.using(uk_tenant):
    assert router.db_for_read(User) == "default"

Working across all regions

Platform-wide reporting needs to read from every regional database. Use the all_regions() context manager, which yields the list of configured region aliases (or ["default"] when BOUNDARY_REGIONS is unset). Pass each alias to .using() explicitly.

from boundary.routing import all_regions
from myapp.models import Patient

with all_regions() as aliases:
    for alias in aliases:
        count = Patient.objects.using(alias).count()
        print(f"{alias}: {count} patients")

Pinning queries to one region

To run queries against a specific region regardless of the active tenant's region, wrap the block in specific_region(). While the context manager is active, RegionalRouter routes every tenant-scoped query to the given region key.

from boundary.routing import specific_region
from myapp.models import Booking

with specific_region("eu-west"):
    bookings = Booking.objects.all()  # hits the eu-west database

# Outside the block, routing reverts to the active tenant's region

If the key passed to specific_region() is not present in BOUNDARY_REGIONS, the router falls back to default for the duration of the block rather than raising.

RegionNotConfiguredError

boundary.exceptions.RegionNotConfiguredError represents the case where a tenant's region is not present in BOUNDARY_REGIONS. It is part of the exception hierarchy and inherits from BoundaryError, so you can catch it alongside the rest of Boundary's exceptions:

from boundary.exceptions import BoundaryError, RegionNotConfiguredError

RegionalRouter itself never raises this exception: a Django router must always return a database alias, so it logs a debug message and falls back to the default alias to keep requests working. When silent fallback to default is unacceptable (for example, data residency), use require_region() to fail loudly instead:

from boundary.routing import require_region
from boundary.exceptions import RegionNotConfiguredError

# Resolve the active tenant's region, or raise if it is not routable.
try:
    alias = require_region()           # uses the active tenant by default
except RegionNotConfiguredError:
    # regions unconfigured, no tenant active, or region not in BOUNDARY_REGIONS
    ...

# Or check a specific tenant, e.g. before a cross-region batch job:
alias = require_region(tenant)

require_region(tenant=None) returns the region alias the tenant routes to, or raises RegionNotConfiguredError if BOUNDARY_REGIONS is unset, no tenant is active, or the tenant's region is not configured. Use it at provisioning time or before a regional job to reject unroutable tenants before any query runs.

Common pitfalls