Cheatsheet for multi-database / multi-tenant Alembic.

Multi-DB template

alembic init -t multidb migrations

alembic.ini:

[alembic]
databases = users, analytics

[users]
sqlalchemy.url = postgresql://...
script_location = migrations/users

[analytics]
sqlalchemy.url = postgresql://...
script_location = migrations/analytics

Multi-DB commands

alembic --name users upgrade head
alembic --name analytics upgrade head

Or iterate in a script.

Schema-per-tenant

# env.py
def run_migrations_online():
    schemas = get_all_tenant_schemas()
    
    connectable = create_engine(...)
    for schema in schemas:
        with connectable.connect() as conn:
            conn.execute(text(f"SET search_path TO {schema}"))
            context.configure(
                connection=conn,
                target_metadata=target_metadata,
                version_table_schema=schema,        # per-schema version table
            )
            with context.begin_transaction():
                context.run_migrations()
    connectable.dispose()

Each tenant’s schema has its own alembic_version.

DB-per-tenant

def run_migrations_online():
    for tenant in get_all_tenants():
        connectable = create_engine(tenant.dsn)
        with connectable.connect() as conn:
            context.configure(connection=conn, target_metadata=target_metadata)
            with context.begin_transaction():
                context.run_migrations()
        connectable.dispose()

Parallel migration runner

import concurrent.futures
from alembic.config import Config
from alembic import command

def migrate_one(tenant):
    cfg = Config("alembic.ini")
    cfg.set_main_option("sqlalchemy.url", tenant.dsn)
    command.upgrade(cfg, "head")

with concurrent.futures.ThreadPoolExecutor(max_workers=8) as ex:
    list(ex.map(migrate_one, tenants))

Watch DB connection counts globally.

Onboarding a new tenant (schema-per-tenant)

async def onboard(tenant_slug: str):
    schema = f"tenant_{tenant_slug}"
    
    # 1. Create schema
    async with engine.connect() as conn:
        await conn.execute(text(f"CREATE SCHEMA {schema}"))
    
    # 2. Apply current head
    cfg = Config("alembic.ini")
    cfg.set_main_option("sqlalchemy.url", DATABASE_URL)
    # ... configure for this schema ...
    command.upgrade(cfg, "head")
    
    # 3. Register
    await register_tenant(tenant_slug, schema)

Or stamp + create_all:

async with engine.begin() as conn:
    await conn.execute(text(f"SET search_path TO {schema}"))
    await conn.run_sync(Base.metadata.create_all)

command.stamp(cfg, "head")    # mark as up-to-date

DB-per-tenant onboarding

async def onboard_db_tenant(slug):
    # 1. Provision DB
    dsn = f"postgresql+asyncpg://app:pass@host/{slug}"
    # ... call infra to create DB ...
    
    # 2. Run migrations
    cfg = Config("alembic.ini")
    cfg.set_main_option("sqlalchemy.url", dsn)
    await asyncio.to_thread(command.upgrade, cfg, "head")
    
    # 3. Register
    await register_tenant(slug, dsn)

CI test per DB

- run: alembic --name users upgrade head
- run: alembic --name analytics upgrade head
- run: pytest tests/

Test each DB.

Drift detection per DB

def test_no_drift_users(users_engine):
    from alembic.autogenerate import compare_metadata
    from alembic.migration import MigrationContext
    
    with users_engine.connect() as conn:
        ctx = MigrationContext.configure(conn, opts={"version_table": "alembic_version"})
        diff = compare_metadata(ctx, UsersBase.metadata)
        assert diff == [], f"users drift: {diff}"

Repeat per DB / per metadata.

Limits

Schema-per-tenant:

  • Postgres handles ~1000 schemas well; beyond → catalog bloat.
  • Sequential migrations slow at scale; parallelize.

DB-per-tenant:

  • Each DB = own backup, monitoring, connections.
  • Useful for compliance / strict isolation.
  • Painful at 100+ tenants without automation.

Common mistakes

  • One alembic_version table shared across tenants — state collapses.
  • Sequential migration of 1000 tenants — hours.
  • Forgetting per-schema version_table_schema.
  • Mixing manual table creation + Alembic — schemas drift.

Read this next

If you want my multi-schema env.py + parallel runner, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .