๐Ÿ—„๏ธ 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 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.


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โ€).