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:
Alembic integration¶
For production, use Alembic for proper migration management.
1. Initialize 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.
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¶
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:
-
Define the new model by subclassing the mixin:
-
Pass the model to the adapter:
-
Generate and apply the migration:
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
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: