Multi-tenant database pool management with per-tenant isolation
| Tier | infrastructure |
| Role | unclassified (baselined) |
| Path | crates/infrastructure/tenant-pool |
| Edition | 2021 |
| Targets | infrastructure_tenant_pool, pg_tenant_guc, provisioning_topology |
| Public items | 12 across 2 modules |
| Tests | 50 |
What it is for
Multi-tenant database pool management with per-tenant isolation.
This crate provides infrastructure for managing database connection pools in a multi-tenant environment where each tenant has their own database.
# Features
- `TenantPoolManager`: Manages a cache of database pools, one per tenant.
Pools are lazily created on first access and automatically evicted when idle.
- `TenantProvisioningService`: Creates and destroys tenant databases,
runs migrations, and updates tenant records.
- Axum Middleware (optional): Extracts tenant ID from requests and injects
the appropriate database pool into handlers.
# Architecture
┌─────────────────────────────────────────────────────────────┐
│ Control Plane DB │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ tenants (id, slug, database_url, status, isolation) │ │
│ │ users, tenant_members, sessions │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
TenantPoolManager
(reads database_url)
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Tenant A │ │ Tenant B │ │ Tenant C │
│ Database │ │ Database │ │ Database │
└──────────┘ └──────────┘ └──────────┘
# Example
use infrastructure_tenant_pool::{TenantPoolManager, PoolManagerConfig};
use sqlx::PgPool;
// Create the control plane pool
let control_pool = PgPool::connect("postgresql://localhost/control").await?;
// Create the pool manager
let config = PoolManagerConfig::default();
let manager = TenantPoolManager::new(control_pool, config);
// Get a pool for a specific tenant
let tenant_id = uuid::Uuid::parse_str("...")?;
let tenant_pool = manager.get_pool(tenant_id).await?;
// Use the pool for queries
let rows = sqlx::query("SELECT * FROM items")
.fetch_all(&tenant_pool)
.await?;
# Axum Integration
When the axum-middleware feature is enabled (default), you can use the tenant middleware in your Axum application:
use axum::{Router, routing::get};
use infrastructure_tenant_pool::{TenantPoolManager, tenant_middleware, TenantCtx};
async fn list_items(TenantCtx(ctx): TenantCtx) -> impl IntoResponse {
// ctx.pool is the tenant's database pool
// ctx.tenant is the Tenant record
let items = sqlx::query_as!(Item, "SELECT * FROM items")
.fetch_all(&ctx.pool)
.await?;
Json(items)
}
let app = Router::new()
.route("/items", get(list_items))
.layer(axum::middleware::from_fn_with_state(
pool_manager.clone(),
tenant_middleware,
));
Capabilities
provisioning (other)
Tenant database provisioning service.
| Item |
|---|
fn create_database_sql(db_name : & str, owner_role : & str, encoding : & str, locale : & str,) -> Result <String, ProvisionError> |
fn harden_database_sql(db_name : & str, app_role : & str) -> Result <Vec <String>, ProvisionError> |
ProvisionRequest
Tenant database provisioning service.
| Item |
|---|
pub struct ProvisionRequest |
ProvisionRequest :: fn new(tenant_id : Uuid, tenant_slug : impl Into <String>) -> Self |
ProvisionResult
Tenant database provisioning service.
| Item |
|---|
pub struct ProvisionResult |
TenantProvisioningService
Tenant database provisioning service.
| Item |
|---|
pub struct TenantProvisioningService |
TenantProvisioningService :: fn new(control_pool : PgPool, config : ProvisioningConfig) -> Self |
TenantProvisioningService :: fn config(& self) -> & ProvisioningConfig |
TenantProvisioningService :: fn control_pool(& self) -> & PgPool |
TenantProvisioningService :: async fn provision(& self, request : ProvisionRequest,) -> Result <ProvisionResult, ProvisionError> |
TenantProvisioningService :: async fn deprovision(& self, tenant_id : Uuid) -> Result <(), ProvisionError> |
TenantProvisioningService :: async fn database_exists(& self, db_name : & str) -> Result <bool, ProvisionError> |
How to use it
From this crate's own rustdoc:
# Example
From this crate's own rustdoc:
# Axum Integration
When the `axum-middleware` feature is enabled (default), you can use the
tenant middleware in your Axum application:
Module structure
infrastructure_tenant_pool
preludeprovisioning
flowchart TD n_infrastructure_tenant_pool["infrastructure_tenant_pool"] n_infrastructure_tenant_pool --> n_prelude["prelude"] n_infrastructure_tenant_pool --> n_provisioning["provisioning"]
Public surface
`provisioning`
| Item | What it is |
|---|---|
pub struct ProvisionRequest | Request to provision a new tenant database. |
ProvisionRequest :: fn new(tenant_id : Uuid, tenant_slug : impl Into <String>) -> Self | Create a new provision request. |
pub struct ProvisionResult | Result of a successful provisioning operation. |
pub struct TenantProvisioningService | Service for provisioning and deprovisioning tenant databases |
TenantProvisioningService :: fn new(control_pool : PgPool, config : ProvisioningConfig) -> Self | Create a new provisioning service. |
TenantProvisioningService :: fn config(& self) -> & ProvisioningConfig | Get the configuration. |
TenantProvisioningService :: fn control_pool(& self) -> & PgPool | Get the control plane pool. |
TenantProvisioningService :: async fn provision(& self, request : ProvisionRequest,) -> Result <ProvisionResult, ProvisionError> | Provision a new database for a tenant |
TenantProvisioningService :: async fn deprovision(& self, tenant_id : Uuid) -> Result <(), ProvisionError> | Deprovision (delete) a tenant's database |
TenantProvisioningService :: async fn database_exists(& self, db_name : & str) -> Result <bool, ProvisionError> | Check if a database exists |
fn create_database_sql(db_name : & str, owner_role : & str, encoding : & str, locale : & str,) -> Result <String, ProvisionError> | Validate database name (prevent SQL injection) |
fn harden_database_sql(db_name : & str, app_role : & str) -> Result <Vec <String>, ProvisionError> | Statements that make the per-database credential an actual boundary |
Re-exports. Exported here, defined elsewhere.
| Export | Defined in |
|---|---|
{IsolationStrategy,Tenant,TenantStatus,TenantTier} | identity_tenant::{IsolationStrategy,Tenant,TenantStatus,TenantTier} |
{IsolationStrategy,Tenant} | identity_tenant::{IsolationStrategy,Tenant} |
{MiddlewareError,PoolError,ProvisionError} | crate::error::{MiddlewareError,PoolError,ProvisionError} |
{MiddlewareError,PoolError,ProvisionError} | error::{MiddlewareError,PoolError,ProvisionError} |
{PoolManagerConfig,ProvisioningConfig} | config::{PoolManagerConfig,ProvisioningConfig} |
{PoolManagerConfig,ProvisioningConfig} | crate::config::{PoolManagerConfig,ProvisioningConfig} |
{PoolManagerStats,TenantPoolManager} | crate::pool_manager::{PoolManagerStats,TenantPoolManager} |
{ProvisionRequest,ProvisionResult,TenantProvisioningService} | crate::provisioning::{ProvisionRequest,ProvisionResult,TenantProvisioningService} |
{ProvisionRequest,ProvisionResult,TenantProvisioningService} | provisioning::{ProvisionRequest,ProvisionResult,TenantProvisioningService} |
{ensure_schemaasensure_fleet_schema,run_tenant_migrations,sandbox_migrations_dir,AccessDecision,FleetError,FleetMigrationConfig,FleetMigrator,FleetRunReport,GateError,SchemaVersion,TenantAccessPolicy,TenantMigrationGate,TenantMigrationSet,TenantResolver,TenantTarget,} | fleet_migrator::{ensure_schemaasensure_fleet_schema,run_tenant_migrations,sandbox_migrations_dir,AccessDecision,FleetError,FleetMigrationConfig,FleetMigrator,FleetRunReport,GateError,SchemaVersion,TenantAccessPolicy,TenantMigrationGate,TenantMigrationSet,TenantResolver,TenantTarget,} |
{extract_tenant_id,resolve_authorized_tenant,tenant_middleware,AuthorizedTenants,TenantContext,TenantCtx,TenantLayer,TenantService,TENANT_ID_HEADER,} | middleware::{extract_tenant_id,resolve_authorized_tenant,tenant_middleware,AuthorizedTenants,TenantContext,TenantCtx,TenantLayer,TenantService,TENANT_ID_HEADER,} |
{single_pool,single_pool_options,PoolManagerStats,TenantPoolManager} | pool_manager::{single_pool,single_pool_options,PoolManagerStats,TenantPoolManager} |
{tenant_middleware,TenantContext,TenantCtx,TenantLayer} | crate::middleware::{tenant_middleware,TenantContext,TenantCtx,TenantLayer} |
Boundary
Reaches into foundation, identity.
Shares tier infrastructure with 82 other crates: infrastructure-acquire, infrastructure-adapters-google-calendar, infrastructure-adapters-google-gmail, infrastructure-adapters-google-places, infrastructure-adapters-google-trends, infrastructure-adapters-shodan, infrastructure-adapters-yelp, infrastructure-agent, … (82 total).
_What this crate deliberately does NOT own is a judgment. No committed registry records one for it, so none is stated here._
Where it sits
| Tier (ontology) | infrastructure |
| Architectural role (taxonomy) | unclassified (baselined) |
| Location | crates/infrastructure/tenant-pool |
| Vocabulary in force (lexicon) | current |
Tier flow. Which tiers this crate's own edges cross.
flowchart LR n_infrastructure["infrastructure"] --> n_foundation["foundation"] n_infrastructure["infrastructure"] --> n_identity["identity"]
Dependencies
Runtime, in this workspace.
| Crate | Tier | Optional | Only on |
|---|---|---|---|
| `foundation-audit-log` | foundation | no | always |
| `foundation-basemodels` | foundation | no | always |
| `identity-tenant` | identity | no | always |
Runtime, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
async-trait | ^0.1 | — | no | always |
axum | ^0.7 | multipart | yes | always |
chrono | ^0.4 | serde, serde | no | always |
dashmap | ^5.5 | — | no | always |
http | ^1.1 | — | yes | always |
humantime | ^2.1 | — | no | always |
serde | ^1 | derive, derive | no | always |
sqlx | ^0.8 | runtime-tokio, postgres, chrono, uuid, json, runtime-tokio, … (10 total) | no | always |
thiserror | ^2 | — | no | always |
tokio | ^1 | full, sync, time | no | always |
tower | ^0.5 | — | yes | always |
tracing | ^0.1 | — | no | always |
uuid | ^1 | v4, v7, serde, js, v4, serde | no | always |
Development, from outside the workspace.
| Crate | Requirement | Features | Optional | Only on |
|---|---|---|---|---|
tokio | ^1 | full, rt-multi-thread, macros | no | always |
Build. None.
Depended on by. 12 workspace crates.
Signal flow — what reaches this crate, and what it reaches.
flowchart LR n_application_communication["application-communication"] -->|uses| SELF n_application_dlp["application-dlp"] -->|uses| SELF n_application_legal_documents["application-legal-documents"] -->|uses| SELF n_application_legal_evidence["application-legal-evidence"] -->|uses| SELF n_application_legal_knowledge["application-legal-knowledge"] -->|uses| SELF n_application_legal_matter["application-legal-matter"] -->|uses| SELF n_application_legal_procedure["application-legal-procedure"] -->|uses| SELF n_application_party_api["application-party-api"] -->|uses| SELF n_application_tenant["application-tenant"] -->|uses| SELF n_operations_privacy_scan_runner["operations-privacy-scan-runner"] -->|uses| SELF n_operations_test_results_ingest["operations-test-results-ingest"] -->|uses| SELF n_platform_privacy_scan_api["platform-privacy-scan-api"] -->|uses| SELF SELF["infrastructure-tenant-pool"] SELF -->|runtime| n_foundation_audit_log["foundation-audit-log"] SELF -->|runtime| n_foundation_basemodels["foundation-basemodels"] SELF -->|runtime| n_identity_tenant["identity-tenant"] classDef self fill:#1f883d,stroke:#1f883d,color:#fff; class SELF self;
Feature flags
| Feature | Enables | On by default |
|---|---|---|
axum | dep:axum | no |
axum-middleware | axum, tower, http | yes |
default | axum-middleware | yes |
http | dep:http | no |
tower | dep:tower | no |
flowchart LR n_axum["axum"] --> n_dep_axum["dep:axum"] n_axum_middleware["axum-middleware"] --> n_axum["axum"] n_axum_middleware["axum-middleware"] --> n_tower["tower"] n_axum_middleware["axum-middleware"] --> n_http["http"] n_default["default"] --> n_axum_middleware["axum-middleware"] n_http["http"] --> n_dep_http["dep:http"] n_tower["tower"] --> n_dep_tower["dep:tower"]
Targets
| Kind | Name | Source |
|---|---|---|
| lib | infrastructure_tenant_pool | `src/lib.rs` |
| test | pg_tenant_guc | `tests/pg_tenant_guc.rs` |
| test | provisioning_topology | `tests/provisioning_topology.rs` |
Error model
No public error type was detected: no public item declares a type named *Error, and no public signature returns one.
Operational characteristics
| Property | Evidence |
|---|---|
| async public surface | yes |
| async runtime | yes |
| database access | yes |
| network I/O | yes |
| unsafe code | none detected |
| environment variables | none detected |
No unsafe block, unsafe fn, unsafe impl or unsafe trait was found by the parser anywhere in this crate's source.
Configuration
No environment variable is read with a literal name anywhere in this crate. A variable whose key is computed at run time cannot be listed here, and is not claimed to be absent.
Related capabilities
12 workspace crates depend on this one: application-communication, application-dlp, application-legal-documents, application-legal-evidence, application-legal-knowledge, application-legal-matter, application-legal-procedure, application-party-api, application-tenant, operations-privacy-scan-runner, operations-test-results-ingest, platform-privacy-scan-api.
Verification
| Kind | Count |
|---|---|
| Unit tests | 43 |
| Integration tests | 7 |
| Examples | 0 |
| Doctests | 2 |
Evidence by module. How often each public module is named by something executable.
| Module | Tests | Examples | Consumers |
|---|---|---|---|
provisioning | 5 | 0 | 0 |
What the tests establish, by name:
tenant_pool_binds_guc_and_rls_filters_cross_tenant_rows—tests/pg_tenant_guc.rsan_invalid_identifier_is_refused_before_any_sql_is_built—tests/provisioning_topology.rscreate_database_keeps_the_template0_and_locale_contract—tests/provisioning_topology.rscreate_database_names_an_explicit_owner—tests/provisioning_topology.rshardening_refuses_invalid_identifiers_too—tests/provisioning_topology.rshardening_revokes_public_connect—tests/provisioning_topology.rsthe_runtime_grant_precedes_the_public_revoke—tests/provisioning_topology.rstest_config_builder—src/config.rstest_default_config—src/config.rstest_generate_db_name—src/config.rstest_provisioning_config—src/config.rsdefault_config_is_safe—src/fleet_migrator.rslock_class_fallback_is_deterministic_per_uuid—src/fleet_migrator.rslock_class_prefers_dense_key—src/fleet_migrator.rssandbox_rejects_escape—src/fleet_migrator.rstest_config_exports—src/lib.rstest_error_display—src/lib.rstest_provisioning_config_exports—src/lib.rsambiguous_membership_without_a_header_is_refused_not_guessed—src/middleware.rsany_permits_any_tenant_but_must_be_chosen_deliberately—src/middleware.rsheader_naming_a_permitted_tenant_is_allowed—src/middleware.rsheader_naming_an_unpermitted_tenant_is_denied—src/middleware.rsmalformed_header_is_rejected_before_any_authorization_decision—src/middleware.rsno_membership_at_all_is_refused—src/middleware.rssole_membership_needs_no_header—src/middleware.rstest_extract_tenant_id_empty_value—src/middleware.rstest_extract_tenant_id_invalid_uuid—src/middleware.rstest_extract_tenant_id_missing_header—src/middleware.rstest_extract_tenant_id_valid—src/middleware.rstest_middleware_error_status_codes—src/middleware.rs- _… 20 more_
Documentation coverage
| Measure | Documented | Total |
|---|---|---|
| Public items with rustdoc | 12 | 12 |
Public modules with a //! block | 1 | 2 |
pie showData
title Public items with rustdoc
"Documented" : 12
"No rustdoc detected" : 0
Metrics
| Metric | Value |
|---|---|
| Rust source files | 7 |
| Source lines | 3069 |
| Code lines | 1971 |
| Public API items | 12 |
| Public modules | 2 |
| Tests | 50 |
| Examples | 0 |
| Cargo features | 5 |
| Direct runtime dependencies | 16 |
| Workspace reverse dependencies | 12 |
pie showData
title Public API by kind
"function" : 2
"method" : 7
"struct" : 3
pie showData
title Rust source composition
"Code" : 1971
"Blank or comment" : 1098
Generation
Rendered by tools-corpus corpus readme from repository evidence alone, renderer schema 2, lexicon current. No model, network service or database was consulted. Regenerate with tools-corpus corpus readme --write; verify with --check.