tenaxum 0.1.0

Tenant-scoped helpers for Axum + sqlx + Postgres. Tenacious about row-level isolation.
Documentation

tenaxum

crates.io docs.rs

Tenant-scoped helpers for Axum + sqlx + Postgres. Tenacious about row-level isolation.

tenaxum exposes two patterns. Pick whichever fits your codebase.

Pattern 1 — pool-scoped (recommended for production apps)

Set app.tenant_id once when a connection is checked out of the pool, reset it on release. Every query during the request is auto-isolated. Zero per-call-site boilerplate.

use axum::{middleware, Router};
use sqlx::postgres::PgPoolOptions;
use tenaxum::pool;

let pool = pool::with_tenant_hooks(PgPoolOptions::new().max_connections(8))
    .connect("postgres://...").await?;

let app = Router::new()
    // ...routes...
    .layer(middleware::from_fn(pool::tenant_scope));

Your auth layer inserts Extension<TenantId>; the tenant_scope middleware reads it and scopes a task-local; the pool hooks read the task-local on every connection checkout. Handlers just write sqlx::query!(...).fetch_one(&pool) and isolation happens automatically.

Pattern 2 — explicit begin_tenant

For background jobs, scripts, and admin paths where the pool hooks aren't wired:

use tenaxum::{TenantId, PgPoolExt};

let mut tx = pool.begin_tenant(tenant).await?;
let rows: Vec<(uuid::Uuid, String)> = sqlx::query_as("SELECT id, body FROM notes")
    .fetch_all(&mut *tx).await?;
tx.commit().await?;

What tenaxum does NOT do

  • JWT decoding. Every app does it differently. Decode in your own middleware, then req.extensions_mut().insert(tenaxum::TenantId(uuid)).
  • RLS policy SQL. See the examples directory for the full pattern with FORCE ROW LEVEL SECURITY + non-superuser-role + WITH CHECK.
  • Scope/permission middleware. Coming in v0.2.

How this was built

tenaxum was written by Claude acting as my coding agent, and was extracted from a production-shape Rust SaaS backend after a multi-agent code audit caught the exact RLS gotcha the proof tests in this repo demonstrate. API design is mine; implementation is Claude's. The library you're reading is the second draft — the first didn't fit the codebase it was extracted from, and plugging it in surfaced the mismatch immediately. See the workspace README for the full story.

If you'd rather not depend on AI-authored code, that's a reasonable position — close the tab, no hard feelings. If you do depend on it, the proof tests are there so you can verify the invariants yourself.

License

MIT.