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 connection by tenant ID.
โ ๏ธ IMPORTANT: Caller is responsible for closing the returned Database connection by calling db.conn.close() and db.engine.dispose() 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โ).