Skip to content

Database Migrations

The library doesn't own your metadata registry. Your models/ package owns every concrete table you subclass from a *Mixin, and your own Base.metadata (SQLAlchemy) or SQLModel.metadata is the single source of truth for Alembic.

Quick start (without Alembic)

For development or simple projects, create tables directly off your own Base:

from sqlmodel import SQLModel

async with engine.begin() as conn:
    await conn.run_sync(SQLModel.metadata.create_all)
from app.core.db import Base   # your DeclarativeBase

async with engine.begin() as conn:
    await conn.run_sync(Base.metadata.create_all)

Alembic integration

For production, use Alembic for proper migration management.

1. Initialize Alembic

pip install fastapi-fullauth[alembic]
alembic init alembic

2. Update env.py

Import your models package so every concrete table you defined registers on Base.metadata, then point Alembic at that metadata.

# alembic/env.py
import app.models  # noqa: F401 = registers all your concrete tables
from sqlmodel import SQLModel

target_metadata = SQLModel.metadata
# alembic/env.py
import app.models  # noqa: F401
from app.core.db import Base

target_metadata = Base.metadata

For async engines, update the run_migrations_online() function to use run_async():

from sqlalchemy.ext.asyncio import async_engine_from_config

def run_migrations_online():
    connectable = async_engine_from_config(
        config.get_section(config.config_ini_section),
        prefix="sqlalchemy.",
    )

    async def do_migrations():
        async with connectable.connect() as connection:
            await connection.run_sync(do_run_migrations)

    import asyncio
    asyncio.run(do_migrations())

3. Generate migrations

alembic revision --autogenerate -m "add fullauth tables"
alembic upgrade head

Tip

Always review autogenerated migrations before applying them. Alembic detects schema changes, but it may miss data migrations or generate incorrect drop/create sequences.

Opt-in tables

Each library table is a mixin that registers only when you subclass it. Subclass only the features you actually use:

Feature Tables Mixins to subclass
Core fullauth_users, fullauth_refresh_tokens UserMixin, RefreshTokenMixin
Roles fullauth_roles, fullauth_user_roles RoleMixin, UserRoleMixin
Permissions fullauth_permissions, fullauth_role_permissions PermissionMixin, RolePermissionMixin
OAuth fullauth_oauth_accounts OAuthAccountMixin
Passkeys fullauth_passkeys PasskeyMixin

Adding features later

When you decide to add a new feature (OAuth, passkeys, permissions), the process is:

  1. Define the new model by subclassing the mixin:

    from fastapi_fullauth.models.sqlmodel import OAuthAccountMixin
    
    class OAuthAccount(OAuthAccountMixin, table=True):
        pass
    
  2. Pass the model to the adapter:

    adapter = SQLModelAdapter(
        ...,
        oauth_account_model=OAuthAccount,
    )
    
  3. Generate and apply the migration:

    alembic revision --autogenerate -m "add oauth accounts table"
    alembic upgrade head
    

You get a clean CREATE TABLE migration for just the new table. Existing tables are unaffected.

Common scenarios

Adding custom fields to the user table

Add fields to your User model, then autogenerate a migration:

class User(UserMixin, table=True):
    display_name: str | None = None  # new field
    avatar_url: str | None = None    # new field
alembic revision --autogenerate -m "add display_name and avatar_url to users"
alembic upgrade head

Existing tables (stamping head)

If you're adding Alembic to a project that already has the tables created (e.g. via create_all), stamp the current state so Alembic knows not to recreate them:

alembic revision --autogenerate -m "initial schema"
# Delete the upgrade/downgrade operations from the generated file,
# since the tables already exist
alembic stamp head