Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tutorial 33: Auth-bound multi-tenancy

Rullst Core can select a tenant from a request hint only after application authentication has inserted a trusted TenantMembership. A hostname, header, query parameter or request body is never sufficient proof of membership.

1. Configure the selection layer

#![allow(unused)]
fn main() {
use axum::{Extension, Router, routing::get};
use rullst_core::{
    multitenant::{TenantConfig, TenantLayer, TenantStrategy},
    security::TenantContext,
};

async fn tenant_dashboard(Extension(tenant): Extension<TenantContext>) -> String {
    format!("Selected workspace: {}", tenant.tenant_id)
}

fn tenant_routes() -> Router {
    let selection = TenantLayer::new(TenantConfig::new(TenantStrategy::Subdomain));
    Router::new()
        .route("/dashboard", get(tenant_dashboard))
        .layer(selection)
}
}

An outer, application-owned authentication layer must first validate the session/token and insert TenantMembership::try_new(...) from trusted identity claims. Without that extension, TenantLayer returns 403 Forbidden. If the requested subdomain is not in the authenticated membership set, it also returns 403. A request that names no tenant (and, with the subdomain strategy, has no with_domain_fallback) uses the membership’s default (TenantMembership::with_default, or the only tenant of a single-tenant membership) and is refused with 403 when there is none. In Axum, remember that subsequently added layers run first; test the final middleware order in-process.

TenantStrategy::Header and TenantStrategy::Parameter are also available, but they remain untrusted selection hints. Query parameters additionally leak more easily through history, referrers and access logs. The built-in subdomain parser reads the Host header, or the HTTP/2 :authority when no Host is sent, and selects the first label only for hostnames with at least three labels; a www label is never a tenant. When the apex domain sits under a multi-label public suffix such as .com.br or .co.uk, set TenantConfig::with_base_domain("escola.com.br") (added in v13): the tenant is then the label immediately to the left of that domain, and the apex itself uses with_domain_fallback (which only the subdomain strategy reads). Custom-domain ownership and trusted-proxy normalization remain application and deployment work.

2. Bind every database query

Tenant selection does not rewrite arbitrary SQL or automatically add a tenant predicate to every ORM query. Bind the authenticated tenant explicitly. This example uses SQLx directly; its #[derive(FromRow)] needs SQLx’s derive feature in your manifest, which Rullst does not enable:

#![allow(unused)]
fn main() {
use rullst_core::security::TenantContext;
use sqlx::{FromRow, PgPool};

#[derive(FromRow)]
struct Invoice {
    id: i64,
    tenant_id: String,
    total_minor: i64,
}

async fn list_tenant_invoices(
    pool: &PgPool,
    tenant: &TenantContext,
) -> Result<Vec<Invoice>, sqlx::Error> {
    sqlx::query_as::<_, Invoice>(
        "SELECT id, tenant_id, total_minor FROM invoices WHERE tenant_id = $1",
    )
    .bind(&tenant.tenant_id)
    .fetch_all(pool)
    .await
}
}

Use database constraints and, where appropriate, database-native row-level security as additional defense. Negative tests must prove that one tenant cannot read, update or delete another tenant’s records through every relevant route, repository, background job and administrative surface.

3. Operational boundaries

  • TenantContext is server-selected state, not a claim accepted from JSON.
  • current_tenant_id() is task-local convenience; passing an explicit TenantContext to domain/storage APIs is easier to audit.
  • TenantCache, TenantStorage, TenantRealtime and TenantPresence provide validated namespaces, but applications still own business authorization and remote provider policy.
  • Cross-process membership updates, custom-domain verification, database policy, audit retention and incident response are not automatic framework guarantees.