Skip to content

Multi-Tenancy

Fullfinity supports multi-tenant deployments where each tenant has its own isolated database.

┌─────────────────────────────────────────────────────────┐
│ Single Codebase │
├─────────────────────────────────────────────────────────┤
│ tenant1.example.com tenant2.example.com tenant3.com │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ DB: t1 │ │ DB: t2 │ │ DB: t3 │ │
│ │ Modules:│ │ Modules:│ │ Modules:│ │
│ │ - core │ │ - core │ │ - core │ │
│ │ - crm │ │ - crm │ │ - crm │ │
│ │ - inv │ │ │ │ - inv │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────┘
  • Data Isolation - Each tenant’s data is in a separate database
  • Independent Modules - Tenants can install different modules
  • Customization - Views can be customized per tenant
  • Security - Complete data separation
  • Scalability - Databases can be on different servers

Tenants are identified by subdomain or host header:

# Request to tenant1.example.com
tenant = get_tenant_from_host(request.headers.get("host"))
# tenant = "tenant1"

Each request uses the tenant’s database pool:

# Get connection pool for tenant
pool = await db_manager.get_pool(tenant_db_name)

Pools are maintained per database:

# Pools are cached and reused
if tenant_db not in connection_pools:
connection_pools[tenant_db] = await create_pool(tenant_db)
multi_tenant:
enabled: true
host_routing: true
default_database: main

Each tenant needs a database:

-- Create tenant database
CREATE DATABASE tenant_acme;
Terminal window
# Initialize/upgrade the tenant's schema (the CLI is `fullfinity-server`)
fullfinity-server -c config.yaml -db tenant_acme -u all

Each tenant can install different modules:

# Install module for specific tenant
await install_module(tenant_db="tenant_acme", module_name="crm")

The module’s:

  • Models are created in the tenant’s database
  • Views are stored in the tenant’s ui_view table
  • Security rules are added to the tenant’s model_access table

Views stored in the database can be customized:

# Tenant-specific view modification
view = await UiView.filter(identifier="contact_form_view").first()
view.arch[0]["content"].append(custom_field)
await view.save()
Terminal window
pg_dump -h localhost -U postgres tenant_acme > tenant_acme_backup.sql
Terminal window
createdb tenant_new
psql -h localhost -U postgres tenant_new < tenant_acme_backup.sql
  • Connection pools are per-database
  • Cache keys include database name
  • Queries are isolated per tenant
  • No cross-tenant data access
  • Tenant context verified on each request
  • Database credentials can differ per tenant
  • Migrations run per database
  • Module updates apply to all tenants
  • Database backups are independent

For data shared across tenants (e.g., countries, currencies):

  1. Store in a shared database
  2. Replicate to tenant databases on sync
  3. Or use read-only access to shared database
async def create_tenant(name: str, admin_email: str):
# 1. Create database
await create_database(f"tenant_{name}")
# 2. Run schema setup
await run_setup(f"tenant_{name}")
# 3. Install core module
await install_module(f"tenant_{name}", "core")
# 4. Create admin user
await create_user(f"tenant_{name}", admin_email)