Configure PostgreSQL Schema-Based Multi-Tenancy
After completing this guide you will have a working PostgreSQL multi-tenant setup where each tenant’s workflows run in an isolated database schema, with optional per-tenant database credentials for defense-in-depth security.
- Cloacina added to your project (
cloacina = "0.1.0"inCargo.toml) - A running PostgreSQL server
- For per-tenant credentials: an admin user with
CREATEDBandCREATEROLEprivileges
Each call to DefaultRunner::with_schema provisions a PostgreSQL schema (if it does not already exist) and returns an executor scoped to that schema:
use cloacina::runner::DefaultRunner;
let database_url = "postgresql://user:pass@localhost/cloacina";
let tenant_a = DefaultRunner::with_schema(database_url, "tenant_a").await?;
let tenant_b = DefaultRunner::with_schema(database_url, "tenant_b").await?;
All workflow state for tenant_a is fully isolated from tenant_b.
Use DatabaseAdmin to create a schema and a dedicated database user in one step. Passing an empty password triggers secure auto-generation:
use cloacina::database::{Database, DatabaseAdmin, TenantConfig};
let admin_db = Database::new(
"postgresql://admin:admin_pass@localhost/cloacina",
"cloacina",
10,
);
let admin = DatabaseAdmin::new(admin_db);
let creds = admin.create_tenant(TenantConfig {
schema_name: "tenant_secure".to_string(),
username: "secure_user".to_string(),
password: "".to_string(), // auto-generate
})?;
// creds exposes: username, password, schema_name, connection_string
Store the returned TenantCredentials in a secrets manager (e.g., HashiCorp Vault, AWS Secrets Manager) and pass the connection string when creating the executor:
let executor = DefaultRunner::with_schema(
&creds.connection_string,
&creds.schema_name,
).await?;
To tear down a tenant and its database user:
admin.remove_tenant(&creds.schema_name, &creds.username)?;
- Database-level isolation – each tenant can only access their own schema
- Audit compliance – PostgreSQL logs attribute operations to the correct user
- Independent credential rotation – rotate one tenant without affecting others
A minimal check that two tenants cannot see each other’s executions:
let tenant_a = DefaultRunner::with_schema(database_url, "test_tenant_a").await?;
let tenant_b = DefaultRunner::with_schema(database_url, "test_tenant_b").await?;
let result_a = tenant_a.execute_async("test_workflow", Context::new()).await?;
let result_b = tenant_b.execute_async("test_workflow", Context::new()).await?;
assert_ne!(result_a.execution_id, result_b.execution_id);
For full integration-testing patterns, see Testing Workflows.
If you do not need PostgreSQL, you can isolate tenants with one SQLite file per tenant:
let executor = DefaultRunner::new("sqlite:///data/tenant_a.db").await?;
Each file is a self-contained database, so isolation is guaranteed by the file system. DatabaseAdmin and per-tenant credentials are not available for SQLite.
- Multi-Tenant Recovery – automatic recovery and migration in multi-tenant deployments
- Testing Workflows – integration testing patterns including tenant isolation tests
- Performance Tuning – connection pool sizing, concurrency limits, and monitoring