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¶
2. Initialize 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¶
6. Apply Migrations¶
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:
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:
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¶
- Model Registration — Register your models with the admin
- Authentication & RBAC — Set up roles and permissions
- Existing Alembic Integration — Integrate with existing Alembic setup