Skip to content

DatabaseAdmin API Reference

The DatabaseAdmin module provides utilities for creating and managing per-tenant database users and schemas in PostgreSQL multi-tenant deployments.

use cloacina::database::{Database, DatabaseAdmin, TenantConfig, TenantCredentials};

Administrative interface for tenant provisioning operations.

pub struct DatabaseAdmin {
// Private fields
}
impl DatabaseAdmin {
/// Create a new database administrator
pub fn new(database: Database) -> Self;
/// Create a complete tenant setup (schema + user + permissions + migrations)
pub fn create_tenant(&self, tenant_config: TenantConfig) -> Result<TenantCredentials, AdminError>;
/// Remove a tenant (user + schema)
pub fn remove_tenant(&self, schema_name: &str, username: &str) -> Result<(), AdminError>;
}

Configuration for creating a new tenant.

pub struct TenantConfig {
/// Schema name for the tenant (e.g., "tenant_acme")
pub schema_name: String,
/// Username for the tenant's database user (e.g., "acme_user")
pub username: String,
/// Password for the tenant user - empty string triggers auto-generation
pub password: String,
}

Credentials returned after tenant creation.

pub struct TenantCredentials {
/// Username of the created tenant user
pub username: String,
/// Password (either provided or auto-generated)
pub password: String,
/// Schema name for the tenant
pub schema_name: String,
/// Ready-to-use connection string for the tenant
pub connection_string: String,
}

Errors that can occur during database administration operations.

#[derive(Debug, thiserror::Error)]
pub enum AdminError {
#[error("Database error: {0}")]
Database(#[from] diesel::result::Error),
#[error("Connection pool error: {0}")]
Pool(#[from] diesel::r2d2::PoolError),
#[error("SQL execution error: {message}")]
SqlExecution { message: String },
#[error("Invalid configuration: {message}")]
InvalidConfig { message: String },
}
use cloacina::database::{Database, DatabaseAdmin, TenantConfig};
// Create admin connection
let admin_db = Database::new(
"postgresql://admin:admin_pass@localhost/cloacina",
"cloacina",
10
);
let admin = DatabaseAdmin::new(admin_db);
// Create tenant with auto-generated password
let creds = admin.create_tenant(TenantConfig {
schema_name: "tenant_acme".to_string(),
username: "acme_user".to_string(),
password: "".to_string(), // Empty = auto-generate
})?;
println!("Created tenant:");
println!(" Username: {}", creds.username);
println!(" Password: {}", creds.password);
println!(" Connection: {}", creds.connection_string);
// Create tenant with custom password
let creds = admin.create_tenant(TenantConfig {
schema_name: "tenant_xyz".to_string(),
username: "xyz_user".to_string(),
password: "custom_secure_password".to_string(),
})?;
// Remove tenant and all associated data
admin.remove_tenant("tenant_xyz", "xyz_user")?;

When password is an empty string, a cryptographically secure password is generated:

  • Length: 32 characters
  • Character Set: 62 characters — uppercase, lowercase, and digits only (symbols are excluded to avoid URL/connection-string escaping issues)
  • Entropy: ~190 bits
  • Generator: Uses rand::rng() (a CSPRNG seeded from the OS) for cryptographic randomness
  1. No Storage: Cloacina never stores passwords
  2. PostgreSQL Hashing: All passwords are hashed with SCRAM-SHA-256 by PostgreSQL
  3. Immediate Return: Credentials are returned to the admin for secure distribution
  4. One-Time View: The plaintext password is only available at creation time

The method performs these operations in a single transaction:

  1. Create Schema

    CREATE SCHEMA IF NOT EXISTS tenant_xyz
  2. Create User

    CREATE USER xyz_user WITH PASSWORD '...'
  3. Grant Permissions

    GRANT USAGE ON SCHEMA tenant_xyz TO xyz_user;
    GRANT CREATE ON SCHEMA tenant_xyz TO xyz_user;
    GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA tenant_xyz TO xyz_user;
    GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA tenant_xyz TO xyz_user;
    ALTER DEFAULT PRIVILEGES IN SCHEMA tenant_xyz GRANT ALL ON TABLES TO xyz_user;
    ALTER DEFAULT PRIVILEGES IN SCHEMA tenant_xyz GRANT ALL ON SEQUENCES TO xyz_user;
  4. Run Migrations

    • Sets search_path to the tenant schema
    • Executes all Cloacina migrations in that schema

The method performs these operations in a single transaction:

  1. Revoke Permissions
  2. Drop User: DROP USER IF EXISTS xyz_user
  3. Drop Schema: DROP SCHEMA IF EXISTS tenant_xyz CASCADE

Production callers: use the HTTP route, not this library call. Per CLOACI-T-0581, the server’s DELETE /v1/tenants/{name} route performs a 4-step orchestrated teardown around this call: (1) revoke API keys for the tenant, (2) evict the tenant’s DefaultRunner from TenantRunnerCache with a bounded drain (--tenant-deletion-drain-timeout-s, default 30s), (3) evict the tenant’s Database from TenantDatabaseCache, then (4) call DatabaseAdmin::remove_tenant (this function). Each step emits a structured audit event with duration. The bare library call skips steps 1-3 and leaves in-process state stale. Use it directly only for testing or for one-shot scripts where no cloacina-server instance is holding tenant caches.

  • Version: PostgreSQL 10+ (for SCRAM-SHA-256 password hashing)
  • Admin Privileges: The admin user needs:
    • CREATEDB privilege
    • CREATEROLE privilege
    • Permission to create schemas
    • Permission to grant privileges
  • Only available when using the postgres feature
  • Not available for SQLite deployments
  1. User Already Exists

    AdminError::SqlExecution { message: "Failed to create user 'xyz_user': ..." }
  2. Invalid Schema Name

    AdminError::InvalidConfig { message: "Schema name cannot be empty" }
  3. Insufficient Privileges

    AdminError::SqlExecution { message: "Failed to create schema: permission denied" }

The credentials returned by DatabaseAdmin are designed to work seamlessly with DefaultRunner:

// Create tenant
let creds = admin.create_tenant(config)?;
// Use credentials with DefaultRunner
let runner = DefaultRunner::with_schema(
&creds.connection_string,
&creds.schema_name
).await?;
// Execute workflows with full isolation
runner.execute(workflow).await?;
  1. Secure Credential Storage: Store returned credentials in a secrets management system
  2. Audit Logging: Log tenant creation/deletion for compliance
  3. Connection String Parsing: Consider parsing the admin connection to build tenant connection strings
  4. Error Recovery: Wrap operations in proper error handling for production use
  5. Resource Limits: Monitor total connection count across all tenants

The Database Admin API is also available in Python with identical functionality:

import cloaca
# Create admin
admin = cloaca.DatabaseAdmin("postgresql://admin:password@localhost/db")
# Create tenant
config = cloaca.TenantConfig(
schema_name="tenant_acme",
username="acme_user",
password="" # Auto-generate
)
credentials = admin.create_tenant(config)
# Use with Python runner
runner = cloaca.DefaultRunner(credentials.connection_string)