This version is in beta. Some features may change before release.

Database router (swappable routing)

Take over how umbral picks the database and read/write target per request - read replicas and custom per-request routing.

Database router

Database routing routes each model to a database statically, by name. A DatabaseRouter lets you take those decisions over yourself, per request - read/write replica split, or routing by any per-request key - without touching the ORM.

Info
The default behavior is unchanged. With no router installed you get today's static per-model routing (`#[umbral(database = "…")]` → `Plugin::database()` → `"default"`). Installing a router is purely additive.

The trait

Code
rust
pub trait DatabaseRouter: Send + Sync {
fn db_for_read(&self, model: &ModelMeta, ctx: &RouteContext) -> Alias { /* default: static alias */ }
fn db_for_write(&self, model: &ModelMeta, ctx: &RouteContext) -> Alias { /* default: static alias */ }
fn allow_relation(&self, a: &ModelMeta, b: &ModelMeta) -> bool { /* default: same database */ }
fn allow_migrate(&self, alias: &str, model: &ModelMeta) -> bool { /* default: migrate on its alias */ }
fn schema_for(&self, ctx: &RouteContext) -> Option<Schema> { None }
}

Every method has a default that reproduces today's behavior, so you only override what you need.

Example: read/write split

Code
rust
use umbral::db::{Alias, DatabaseRouter, RouteContext};
use umbral::migrate::ModelMeta;
 
struct ReplicaRouter;
impl DatabaseRouter for ReplicaRouter {
fn db_for_read(&self, _m: &ModelMeta, _c: &RouteContext) -> Alias { Alias::new("replica") }
fn db_for_write(&self, _m: &ModelMeta, _c: &RouteContext) -> Alias { Alias::new("default") }
}
 
App::builder()
.database("default", primary_pool)
.database("replica", replica_pool)
.router(ReplicaRouter)
.build()?;

Reads now resolve to the replica pool, writes to default.

Every read terminal (fetch, first, count, exists, get, aggregate, …) calls db_for_read; every write terminal (create, bulk_create, update_values, delete, …) calls db_for_write. .on(&pool) is a hard override that bypasses the router entirely.

Info

Read-after-write consistency. get_or_create and update_or_create run their existence check against the write pool, not the read replica, so they can't miss a just-written row on a lagging replica and spuriously insert a duplicate.

Per-request context

To route by a per-request value (a region, a shard key, a tenant id), populate a request-scoped [RouteContext] from each request and read it in your router:

Code
rust
App::builder()
.route_context(|req| {
let tenant = req.headers().get("X-Tenant").and_then(|v| v.to_str().ok());
match tenant {
Some(t) => RouteContext::new().with_tenant(TenantKey::new(t)),
None => RouteContext::new(),
}
})
.router(MyTenantRouter)
.build()?;

The context rides a task-local for the whole request, so every ORM call inside the handler sees it - no threading required. Background jobs do not inherit it: a spawned task gets the default context (no tenant) unless it opts in explicitly via umbral::db::route_context_scope(ctx, fut). This is deliberate - a worker never silently runs as the wrong tenant.

Multitenancy is not a feature yet

Warning

umbral ships no multitenancy system - no Tenant model, no per-tenant schema migrations, no tenant provisioning. Don't reach for the router expecting turnkey schema- or database-per-tenant tenancy; it gives you the low-level routing primitive (which database a query uses), not the management around it.

The trait does carry a schema_for(ctx) -> Option<Schema> hook for qualifying queries with a schema name, but it's a low-level building block, not a supported tenancy feature, and is intentionally undocumented here until the management layer (deferred) lands.

See also

  • Database routing - the static, by-name side: named pools, per-model / per-plugin routing, per-database migrations, and the cross-DB FK guard.
  • examples/read-replica/ - a working read/write split end to end.
  • DatabaseRouter foundation spec: docs/superpowers/specs/2026-06-16-database-router-foundation-design.md.
ormdatabaseroutingreplicas