Skip to content

Adapters

What is an adapter

Adapters decouple the authentication logic from the database. The library defines abstract interfaces for user management, token storage, roles, OAuth, and passkeys. Concrete adapters implement these interfaces for specific ORMs.

The library ships adapters for SQLModel, SQLAlchemy, Tortoise ORM, and Beanie (MongoDB). You can write your own for any other data store by implementing the abstract interface.

Available adapters

Adapter Backend Install
SQLModel Any SQLAlchemy-supported DB pip install fastapi-fullauth[sqlmodel]
SQLAlchemy Any SQLAlchemy-supported DB pip install fastapi-fullauth[sqlalchemy]
Tortoise ORM Any Tortoise-supported DB pip install fastapi-fullauth[tortoise]
Beanie MongoDB pip install fastapi-fullauth[beanie]

Choosing an adapter

  • SQLModel: recommended for most projects. Clean model definitions, good type support. Use SQLite for prototyping.
  • SQLAlchemy: use if your project already uses SQLAlchemy's declarative base.
  • Tortoise ORM: use if your project is built on Tortoise's async, Django-like ORM.
  • Beanie: use if your data lives in MongoDB. Roles and permissions are embedded on the document rather than joined, and refresh-token rotation stays atomic without a replica set.

All four adapters support the same features. The difference is in model definition style and the data store they bind to.

Adapter architecture

Core interface

AbstractUserAdapter defines the contract every adapter must implement:

  • User CRUD: get_user_by_id(), get_user_by_email(), get_user_by_field(), create_user(), update_user(), delete_user()
  • Passwords: get_hashed_password(), set_password()
  • Refresh tokens: store_refresh_token(), get_refresh_token(), revoke_refresh_token(), revoke_refresh_token_family(), revoke_all_user_refresh_tokens()
  • Verification: set_user_verified(), get_user_roles()

Optional mixins

Mixins add capabilities to your adapter. The library checks isinstance() at startup to decide which routers to mount. If your adapter doesn't inherit a mixin, the corresponding feature is simply not available - no dead endpoints, no errors.

Mixin Enables Required model
RoleAdapterMixin Admin router, require_role() RoleMixin
PermissionAdapterMixin require_permission() PermissionMixin, RolePermissionMixin
OAuthAdapterMixin OAuth router OAuthAccountMixin
PasskeyAdapterMixin Passkey router PasskeyMixin

Model mixins

The library provides SQLAlchemy and SQLModel mixins for database tables. You subclass them to create concrete tables in your app's metadata. The library never ships its own tables - your app owns every table definition, which means Alembic migrations work naturally. (Tortoise ships an equivalent set of abstract model mixins with a slightly different shape - native M2M relations, no association tables; see the Tortoise adapter.)

Mixin Default table name Purpose
UserMixin fullauth_users User accounts
RefreshTokenMixin fullauth_refresh_tokens Stored refresh tokens
RoleMixin fullauth_roles Role definitions
UserRoleMixin fullauth_user_roles User-role assignments
OAuthAccountMixin fullauth_oauth_accounts Linked OAuth providers
PasskeyMixin fullauth_passkeys WebAuthn credentials
PermissionMixin fullauth_permissions Permission definitions
RolePermissionMixin fullauth_role_permissions Role-permission mappings

You only need the mixins for features you use. A minimal setup needs just UserMixin and RefreshTokenMixin.

Custom adapters

Not using SQL? Subclass AbstractUserAdapter for core auth and add a mixin per feature you need. The library checks isinstance() at startup, so routers for unimplemented features never mount.

See Writing a custom adapter for a complete, runnable worked example (an in-memory store), the key method contracts, and how to opt into roles, permissions, OAuth, passkeys, and sessions.

Custom schemas

Define your own user schemas by extending UserSchema and CreateUserSchema, then pass them to the adapter:

from fastapi_fullauth import UserSchema, CreateUserSchema

class MyUserSchema(UserSchema):
    display_name: str = ""

class MyCreateSchema(CreateUserSchema):
    display_name: str = ""

adapter = SQLModelAdapter(
    session_maker=session_maker,
    user_model=User,
    refresh_token_model=RefreshToken,
    user_schema=MyUserSchema,
    create_user_schema=MyCreateSchema,
)

Choosing a primary key type

User ids are UUIDs by default. To use integer, sequence, or string keys instead, parameterise UserSchema with the type your table stores:

class MyUserSchema(UserSchema[int]):   # integer or sequence keys
    display_name: str = ""

class MyUserSchema(UserSchema[str]):   # string keys
    display_name: str = ""

That is the only wiring needed. The adapter reads the key type off the schema and converts token subjects back to it, so login, refresh, sessions, verification, and password reset all work unchanged. Writing class MyUserSchema(UserSchema) keeps UUID keys, which is why existing projects need no edits.

The schema and your table have to agree. If they disagree the adapter raises at construction, naming both types, rather than letting every request fail as a confusing 401:

User id type mismatch: the user schema (MyUserSchema) declares id: int,
but the user model (User) stores UUID.

Warning

Sequential integer keys are guessable, so anywhere you expose a user id becomes enumerable. UUIDv7 keys avoid that while staying index-friendly. Prefer integers when an existing schema requires them, not by default.

The UserSchema base class defines PROTECTED_FIELDS - a set of fields that can't be updated via PATCH /me. By default this includes id, email, hashed_password, is_active, is_verified, is_superuser, roles, password, created_at, and refresh_tokens. If your custom schema adds fields that should also be protected from profile updates, extend this set.

If your app uses roles, add roles to your custom schema:

class MyUserSchema(UserSchema):
    roles: list[str] = Field(default_factory=list)