Skip to content

Alembic Setup

FastAPI Admin Kit provides built-in models for authentication (users, roles, permissions) and audit logging. This guide shows how to set up Alembic to manage database migrations for both admin tables and your application models.

Quick Start with fak init-alembic

The easiest way to get started is using the built-in CLI command:

# For a new project
fak init-alembic --app myapp:app --auto-migrate

# For an existing database (baseline migration)
fak init-alembic --app myapp:app --baseline

This command: 1. Creates alembic.ini with proper configuration 2. Creates alembic/env.py that imports admin models from fastapi_admin_kit.migrations.models 3. Creates alembic/script.py.mako template 4. Optionally auto-generates the initial migration (--auto-migrate) 5. Optionally creates a baseline migration for existing databases (--baseline)

What init-alembic Does

myproject/
├── alembic.ini          # Alembic configuration
├── alembic/
│   ├── env.py           # Migration environment (imports admin models)
│   ├── script.py.mako   # Migration template
│   └── versions/        # Migration scripts

The generated alembic/env.py includes:

# Import admin models (materialized from schemas)
from fastapi_admin_kit.migrations.models import Base as AdminBase

# Import your app models
# from myapp.models import Base as AppBase

# Combine metadata for autogenerate
target_metadata = [AdminBase.metadata]
# target_metadata.append(AppBase.metadata)  # Add your models

All admin tables are included by default

AdminBase.metadata is a single shared MetaData registry: every model imported from fastapi_admin_kit.migrations.models (users, roles, permissions, junction tables, refresh tokens, audit log, TOTP, login attempts, and the admin_ai_* tables) registers itself on it. Since .metadata refers to that whole registry — not just one model — running alembic revision --autogenerate will generate all admin tables at once. It is your responsibility to specify which models you want.

For an initial init db migration this is usually exactly what you want. If you only want a subset of the tables, see Tracking Only Specific Tables.

Manual Alembic Setup

If you prefer manual setup or have an existing Alembic configuration:

1. Install Alembic

pip install alembic

2. Initialize Alembic

alembic init alembic

3. Configure alembic/env.py

Replace the contents of alembic/env.py:

import asyncio
import sys
from logging.config import fileConfig
from pathlib import Path

from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config

from alembic import context

# Add project root to path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

# Import admin models (materialized from schemas)
from fastapi_admin_kit.migrations.models import Base as AdminBase

# Import your application models
# from myapp.models import Base as AppBase

# Combine metadata for autogenerate
target_metadata = [AdminBase.metadata]
# target_metadata.append(AppBase.metadata)  # Add your models

config = context.config

if config.config_file_name is not None:
    fileConfig(config.config_file_name)


def run_migrations_offline() -> None:
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
    )
    with context.begin_transaction():
        context.run_migrations()


def do_run_migrations(connection: Connection) -> None:
    context.configure(connection=connection, target_metadata=target_metadata)
    with context.begin_transaction():
        context.run_migrations()


async def run_async_migrations() -> None:
    connectable = async_engine_from_config(
        config.get_section(config.config_ini_section, {}),
        prefix="sqlalchemy.",
        poolclass=pool.NullPool,
    )
    async with connectable.connect() as connection:
        await connection.run_sync(do_run_migrations)
    await connectable.dispose()


def run_migrations_online() -> None:
    asyncio.run(run_async_migrations())


if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()

4. Configure Database URL

Edit alembic.ini:

[alembic]
script_location = alembic
sqlalchemy.url = sqlite+aiosqlite:///./your_database.db
# For PostgreSQL:
# sqlalchemy.url = postgresql+asyncpg://user:pass@localhost:5432/dbname

5. Generate Initial Migration

alembic revision --autogenerate -m "init admin models"

6. Apply Migrations

alembic upgrade head

Production vs Development Mode

FastAPI Admin Kit supports two modes:

Mode Setting Behavior
Development (default) use_alembic=False Uses create_all() + auto-migration (adds missing columns)
Production use_alembic=True Expects Alembic to manage schema; skips create_all()

In Your Application

from fastapi_admin_kit import Admin
from fastapi_admin_kit.config import BehaviorConfig

admin = Admin(
    app=app,
    engine=engine,
    # ... other config ...
    behavior=BehaviorConfig(use_alembic=True),  # Production mode
)

In Lifespan (Production)

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Run alembic upgrade head on startup
    from alembic.config import Config
    from alembic import command

    alembic_cfg = Config("alembic.ini")
    command.upgrade(alembic_cfg, "head")

    await admin.setup(app)
    yield

CLI Commands

Initialize Alembic

# New project with auto-generated initial migration
fak init-alembic --app myapp:app --auto-migrate

# Existing project with database (creates baseline)
fak init-alembic --app myapp:app --baseline

# Force overwrite existing alembic config
fak init-alembic --app myapp:app --force

Run Migrations (Production)

# Run all pending migrations (equivalent to alembic upgrade head)
fak migrate-alembic

# Run to specific revision
fak migrate-alembic <revision>

# Use custom app path to find alembic.ini
fak migrate-alembic --app myapp:app

Dev Mode Migrations (Legacy)

# Add missing columns / recreate tables (dev only)
fak migrate User Product

# Convert old permissions format
fak migrate-permissions

Existing Database Migration (Baseline)

If you have an existing database created with create_all():

# Create baseline migration and stamp as applied
fak init-alembic --app myapp:app --baseline

# Or manually:
alembic revision -m "baseline_existing_schema"
# Edit the migration to match your current schema
alembic stamp head

Adding Your Models to Migrations

In alembic/env.py, add your application's metadata:

from fastapi_admin_kit.migrations.models import Base as AdminBase
from myapp.models import Base as AppBase  # Your models

target_metadata = [AdminBase.metadata, AppBase.metadata]

Then autogenerate will include both admin and app tables:

alembic revision --autogenerate -m "add product table"

Tracking Only Specific Tables

Because AdminBase.metadata is a shared registry, autogenerate compares every admin table against your database. If you only want Alembic to track a subset of tables, use the include_name (or include_object) hook in alembic/env.py. Excluded tables are neither reflected nor compared, so no statements are generated for them at all.

Filter by table name:

def include_name(name, type_, parent_names):
    if type_ == "table":
        return name in {"admin_users", "admin_roles"}  # only these tables
    return True


# Pass it to every context.configure() call — both offline and online:
def run_migrations_offline() -> None:
    ...
    context.configure(
        url=url,
        target_metadata=target_metadata,
        include_name=include_name,
        ...
    )


def do_run_migrations(connection: Connection) -> None:
    context.configure(
        connection=connection,
        target_metadata=target_metadata,
        include_name=include_name,
    )

Or filter by object, which also lets you inspect the Table itself:

TRACKED = {"admin_users", "admin_roles"}


def include_object(obj, name, type_, reflected, compare_to):
    if type_ == "table" and obj.metadata is AdminBase.metadata:
        return name in TRACKED  # admin tables: only the tracked set
    return True  # everything else (e.g. your app tables) is kept

The include_object variant keeps all of your app tables while restricting admin tables to the tracked set.

Alternatively, you can simply delete unwanted statements from a generated migration before applying it — fine for one-offs, but the hooks keep every future autogenerate consistent.

Junction Tables

The admin models include two junction tables for many-to-many relationships: - admin_user_roles — User ↔ Role - admin_role_permissions — Role ↔ Permission

These are automatically created via SQLAlchemy relationships and included in migrations.

AI tables and migrations

AdminBase.metadata always includes the four admin_ai_* tables (admin_ai_usage_log, admin_ai_conversations, admin_ai_messages, admin_ai_attachments), regardless of the ai_enabled flag. The AI schemas are materialized unconditionally in migrations/models.py and get_admin_metadata() is never filtered.

This means managing the AI schema is independent of the ai_enabled flag: if your initial Alembic revision was autogenerated (e.g. via fak init-alembic --auto-migrate), the admin_ai_* CREATE TABLE statements are already present, so enabling or disabling AI later never requires a new migration. If your database somehow lacks them (e.g. an older database used with --baseline), the next alembic revision --autogenerate && alembic upgrade head adds four plain CREATE TABLE statements.

In development mode (use_alembic=False), Admin.setup() also creates the AI tables automatically whenever ai_enabled=True, so no migration is needed there either.

Troubleshooting

"Table already exists" on initial migration

If tables were created via create_all() before Alembic:

# Option 1: Baseline (recommended)
fak init-alembic --baseline

# Option 2: Stamp head manually
alembic stamp head

Import errors in env.py

Ensure your project root is in sys.path:

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

Async engine issues

The generated env.py uses async_engine_from_config for async migrations. For sync engines, use the sync variant in the template.

Next Steps