from nbdev.showdoc import show_doc๐๏ธ Tenant Databases
๐ฏ Overview
Each tenant gets their own isolated database containing:
| Model | Purpose |
|---|---|
๐ค TenantUser |
Local user profiles linked to global identity |
๐ TenantPermission |
Fine-grained resource permissions |
โ๏ธ TenantSettings |
Tenant-wide configuration |
๐๏ธ Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ HOST DATABASE โ
โ โโโโโโโโโโโโโโโ โ
โ โ TenantCatalog โ โโโบ Maps tenant_id โ database URL โ
โ โโโโโโโโฌโโโโโโโ โ
โโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ get_or_create_tenant_db(tenant_id)
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐๏ธ TENANT DATABASE โ
โ (Isolated per tenant) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๐ค TenantUser โ Local profile (links to GlobalUser.id) โ
โ ๐ TenantPermission โ Resource-level access control โ
โ โ๏ธ TenantSettings โ Timezone, currency, feature flags โ
โ ๐ช WebhookEvent โ Idempotent webhook processing โ
โ ๐ WebhookSecret โ HMAC secrets per webhook source โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๐ [Your App Tables] โ Transactions, budgets, etc. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key Principle: Tenant data is completely isolated โ No cross-tenant data leakage possible
๐ Tenant Connection
โ ๏ธ Naming Convention: Use underscores (not hyphens) in
tenant_idvalues.Database names are derived from
tenant_id(e.g.,tenant_acme_001โt_tenant_acme_001_db). PostgreSQL and SQLite identifiers work best with alphanumeric characters and underscores.โ Good:
tenant_acme_001,tenant_finxplorer_prodโ Avoid:tenant-acme-001,tenant.finxplorer.prod
get_or_create_tenant_db() handles the full lifecycle:
- ๐ Check if tenant exists in host database
- ๐๏ธ Create physical database if new (PostgreSQL)
- ๐ Register tenant in
TenantCatalog - ๐ Return connection to tenant database
| Parameter | Description |
|---|---|
tenant_id |
Unique tenant identifier (from Membership) |
tenant_name |
Optional display name (defaults to tenant_id) |
Returns: Database connection to the tenantโs isolated database
get_or_create_tenant_db
def get_or_create_tenant_db(
tenant_id:str, tenant_name:str=None
):
Get or create a tenant database handle by tenant ID.
The package caches tenant routing, SQLAlchemy engines, and reflected table metadata, but each call returns a fresh live connection bound to its own cloned metadata object. Callers should still close the returned connection when done.
๐ฆ Core Tenant Models
These models provide the infrastructure every tenant needs. Your app-specific models (transactions, budgets, etc.) build on top of these.
TenantSettings
def TenantSettings(
args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):
Tenant-wide configuration and feature flags.
TenantPermission
def TenantPermission(
args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):
Fine-grained resource permission for a tenant user.
TenantUser
def TenantUser(
args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):
Local user profile linked to GlobalUser in host database.
๐ Model Details
| Model | Table Name | Primary Key | Description |
|---|---|---|---|
TenantUser |
core_tenant_users |
id |
Links to GlobalUser.id, stores local role & preferences |
TenantPermission |
core_permissions |
id |
Resource + action permissions (RBAC) |
TenantSettings |
core_settings |
id |
Timezone, currency, feature flags |
๐ง Schema Initialization
init_tenant_core_schema() creates all infrastructure tables in a tenant database:
tenant_db = get_or_create_tenant_db("tenant_abc")
tables = init_tenant_core_schema(tenant_db)
# Access tables via returned dict
tables['tenant_users'].insert(user)
tables['settings'].insert(settings)
Returns: Dictionary of table accessors for all core models
init_tenant_core_schema
def init_tenant_core_schema(
tenant_db:Database
):
Create all core tenant tables and return table accessors.
๐ Quick Start
from fh_saas.db_tenant import get_or_create_tenant_db, init_tenant_core_schema, TenantUser
from fh_saas.db_host import gen_id, timestamp
# Get or create tenant database
tenant_db = get_or_create_tenant_db("tenant_abc123", "Acme Corp")
# Initialize core schema
tables = init_tenant_core_schema(tenant_db)
# Add a tenant user (linked to GlobalUser.id)
user = TenantUser(
id="global_user_xyz", # Must match GlobalUser.id
display_name="John Doe",
local_role="admin",
created_at=timestamp()
)
tables['tenant_users'].insert(user)
tenant_db.conn.commit()๐ก Tip: The id field in TenantUser must match the GlobalUser.id from the host database to maintain identity linking.
๐ Role-Based Access Control (RBAC)
Every tenant has a three-tier role system for controlling access:
Role Hierarchy
| Role | Level | Description |
|---|---|---|
admin |
3 | Full access to all resources and settings |
editor |
2 | Can view and modify data, but not settings |
viewer |
1 | Read-only access to data |
Role Assignment Rules
- Tenant owner (from host
Membership.role='owner') โ automatically getsadminrole - Other users โ assigned explicitly by admin via
TenantUser.local_role - No defaults โ admins must explicitly assign roles when inviting users
TenantUser.local_role
The local_role field in TenantUser stores the userโs role within the tenant:
# Example: Admin adds a new user as editor
new_user = TenantUser(
id=global_user_id, # Must match GlobalUser.id
display_name="Jane Doe",
local_role="editor", # Assigned by admin
created_at=timestamp()
)
tables['tenant_users'].insert(new_user)Fine-Grained Permissions (Optional)
For advanced use cases, the core_permissions table allows resource-level control:
# Example: Grant user edit access to transactions only
permission = TenantPermission(
id=gen_id(),
user_id=tenant_user.id,
resource="transactions",
action="edit",
granted=True,
created_at=timestamp()
)
tables['permissions'].insert(permission)| Field | Description |
|---|---|
resource |
What the permission applies to (e.g., โtransactionsโ, โbudgetsโ) |
action |
What action is allowed (e.g., โviewโ, โeditโ, โdeleteโ) |
granted |
True = allowed, False = explicitly denied |
๐ก Tip: Most apps only need the
local_rolefield. Usecore_permissionswhen you need per-resource control (e.g., โUser X can view transactions but not budgetsโ).