Skip to content

Event Hooks

Hooks let you run custom logic when auth events happen - send emails, log analytics, sync with external systems - without modifying the core auth flows.

How hooks work

The EventHooks system is a simple async event emitter. You register callbacks with fullauth.hooks.on(), and the library calls them automatically when events occur.

Key behaviors:

  • Hooks fire after the side effect commits. When after_register fires, the user is already in the database. When after_logout fires, the token is already blacklisted. You can't cancel an operation from a hook.
  • Error isolation: if a hook raises an exception, it's caught and logged to the fastapi_fullauth.hooks logger. The route returns its normal response. Other hooks for the same event still run.
  • Hooks run in registration order, sequentially (not concurrently).
  • after_register, send_verification_email, and send_password_reset_email run after the response is sent, as FastAPI background tasks. Their latency (typically an email send) would otherwise make responses for real accounts slower than for unknown ones, revealing which emails are registered. Other hooks run before the response.

Registering hooks

The recommended way is to use on() as a decorator - it registers the callback and returns it unchanged, keeping the registration next to the function:

fullauth = FullAuth(adapter=adapter, config=FullAuthConfig(SECRET_KEY="..."))

@fullauth.hooks.on("after_register")
async def on_register(user):
    print(f"New user: {user.email}")

The decorator needs fullauth to exist first, so define it before the decorated functions.

Equivalently, you can pass the callback directly - useful when registering a function defined elsewhere:

fullauth.hooks.on("after_register", on_register)

Mistakes surface at registration

on() validates as you register: an unknown event name (a typo like after_registr) raises a UserWarning with a "did you mean" hint, and a callback whose signature can't accept the event's arguments warns too - so a misregistered hook fails loudly here instead of silently never firing. If you emit your own custom events with fullauth.hooks.emit("my_event", ...), the unknown-event warning is expected and safe to ignore.

Available events

Event Callback signature When it fires
after_register async def(user: UserSchema) After user account is created
after_login async def(user: UserSchema) After successful password login
after_logout async def(user_id: UserID) After access token is blacklisted
after_password_change async def(user: UserSchema) After password is updated
after_password_reset async def(user: UserSchema) After password reset completes
after_email_verify async def(user: UserSchema) After email is verified
after_oauth_login async def(user: UserSchema, provider: str, is_new_user: bool) After OAuth login succeeds
after_oauth_register async def(user: UserSchema, user_info: OAuthUserInfo) After a new user is created via OAuth
after_oauth_link async def(user: UserSchema, provider: str) After a signed-in user links a provider
after_oauth_unlink async def(user: UserSchema, provider: str) After a signed-in user unlinks a provider
send_verification_email async def(email: str, token: str) When email verification is requested
send_password_reset_email async def(email: str, token: str) When password reset is requested

Examples

Sending emails

The library generates verification and reset tokens but does not send emails itself. Register hooks to deliver them:

@fullauth.hooks.on("send_verification_email")
async def send_verification_email(email: str, token: str):
    verify_url = f"https://myapp.com/verify?token={token}"
    await my_email_service.send(
        to=email,
        subject="Verify your email",
        body=f"Click here to verify: {verify_url}",
    )

@fullauth.hooks.on("send_password_reset_email")
async def send_password_reset_email(email: str, token: str):
    reset_url = f"https://myapp.com/reset-password?token={token}"
    await my_email_service.send(
        to=email,
        subject="Reset your password",
        body=f"Click here to reset: {reset_url}",
    )

Note

If you don't register a send_verification_email hook, the verification token is generated but never delivered. Same for password reset. The endpoint still returns a success response. To catch this, init_app() emits a UserWarning when the verify router is mounted but the matching send_* hook is missing - register the hook before init_app(), or drop the router with init_app(include_routers=...) if you don't use those flows.

Audit logging

import logging

audit_log = logging.getLogger("audit")

@fullauth.hooks.on("after_login")
async def log_login(user):
    audit_log.info("Login: email=%s id=%s", user.email, user.id)

@fullauth.hooks.on("after_logout")
async def log_logout(user_id):
    audit_log.info("Logout: user_id=%s", user_id)

Verification email on registration

Registering does not send a verification email by itself: /register only fires after_register, and send_verification_email is emitted by POST /verify-email/request, which requires the user to be signed in already. To mail a link at signup, mint the token in the hook:

from fastapi_fullauth.flows import create_email_verification_token


@fullauth.hooks.on("after_register")
async def send_verification(user):
    token = await create_email_verification_token(
        fullauth.adapter, fullauth.token_engine, user.id
    )
    if token:  # None when the account is already verified
        await mailer.send_verification(user.email, token)

This hook runs after the response is sent, so the time your mail provider takes cannot reveal whether the address was already registered.

Post-registration setup

Create default resources for new users:

@fullauth.hooks.on("after_register")
async def create_defaults(user):
    await create_workspace(owner_id=user.id, name="My Workspace")
    await create_user_profile(user_id=user.id)

OAuth login tracking

@fullauth.hooks.on("after_oauth_login")
async def track_oauth(user, provider, is_new_user):
    if is_new_user:
        await analytics.track("signup", {"provider": provider, "email": user.email})
    else:
        await analytics.track("login", {"provider": provider, "email": user.email})

Multiple hooks per event

You can register multiple callbacks for the same event. They run in registration order:

fullauth.hooks.on("after_register", send_welcome_email)
fullauth.hooks.on("after_register", create_default_workspace)
fullauth.hooks.on("after_register", track_signup_analytics)

Error handling in hooks

A hook that raises is caught and logged; the next hook still runs and the route returns its normal response. Auth never returns a 500 because of a notification or analytics failure.

If you need to handle errors within a hook (e.g. retry logic, fallback behavior), use try/except inside your callback:

async def send_welcome_email(user):
    try:
        await email_service.send(to=user.email, subject="Welcome!")
    except Exception:
        logging.getLogger("myapp").warning("Welcome email failed for %s", user.email)

To see hook errors from the library itself, configure the logger:

import logging
logging.getLogger("fastapi_fullauth.hooks").setLevel(logging.DEBUG)

Type safety

The hooks.on() method uses @overload with Protocol types so your IDE can autocomplete callback signatures. The available protocol types are:

Protocol Used by Signature
AfterUserHook Most user events async def(user: UserSchema)
AfterLogoutHook after_logout async def(user_id: UserID)
EmailHook Email events async def(email: str, token: str)
AfterOAuthLoginHook after_oauth_login async def(user: UserSchema, provider: str, is_new_user: bool)
AfterOAuthRegisterHook after_oauth_register async def(user: UserSchema, user_info: OAuthUserInfo)
AfterOAuthLinkHook after_oauth_link, after_oauth_unlink async def(user: UserSchema, provider: str)

You can import these from fastapi_fullauth.hooks if you want to type-annotate your callbacks:

from fastapi_fullauth.hooks import AfterUserHook

on_login: AfterUserHook = my_login_handler