๐Ÿ—„๏ธ Tenant Databases

Isolated per-tenant data storage with user profiles, permissions, and settings.

๐ŸŽฏ 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_id values.

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

from nbdev.showdoc import show_doc

get_or_create_tenant_db() handles the full lifecycle:

  1. ๐Ÿ” Check if tenant exists in host database
  2. ๐Ÿ—„๏ธ Create physical database if new (PostgreSQL)
  3. ๐Ÿ“ Register tenant in TenantCatalog
  4. ๐Ÿ”Œ 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


source

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.


source

TenantSettings


def TenantSettings(
    args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):

Tenant-wide configuration and feature flags.


source

TenantPermission


def TenantPermission(
    args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):

Fine-grained resource permission for a tenant user.


source

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


source

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

  1. Tenant owner (from host Membership.role='owner') โ†’ automatically gets admin role
  2. Other users โ†’ assigned explicitly by admin via TenantUser.local_role
  3. 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_role field. Use core_permissions when you need per-resource control (e.g., โ€œUser X can view transactions but not budgetsโ€).