SeaORM Hooks
Intercept SeaORM-backed auth operations with lifecycle hooks.
SeaORM hooks let you run custom logic before and after auth writes. Use them for validation, auditing, provisioning, or rejecting operations while keeping the public auth runtime separate from your persistence integration.
Setup
These examples assume you've already defined AppAuthSchema and a database: DatabaseConnection. If you want a complete setup from entity definitions through Axum mounting, start with the Axum integration guide.
use std::sync::Arc;
use async_trait::async_trait;
use better_auth::AuthSchema;
use better_auth::seaorm::{HookControl, SeaOrmHookContext, SeaOrmHooks, SeaOrmStore};
struct AuditHook;
#[async_trait]
impl SeaOrmHooks<AppAuthSchema> for AuditHook {
async fn after_create_user(
&self,
user: &<AppAuthSchema as AuthSchema>::User,
_ctx: &SeaOrmHookContext<'_>,
) -> better_auth::AuthResult<()> {
println!("User created: {:?}", user.email());
Ok(())
}
}
let store = SeaOrmStore::<AppAuthSchema>::new(config.clone(), database)
.hook(AuditHook);
let auth = BetterAuth::<AppAuthSchema>::new(config)
.store(store)
.build()
.await?;The SeaOrmHooks Trait
All methods have default no-op implementations. Override only what you need.
#[async_trait]
pub trait SeaOrmHooks<S: AuthSchema>: Send + Sync {
async fn before_create_user(
&self,
user: &mut CreateUser,
ctx: &SeaOrmHookContext<'_>,
) -> AuthResult<HookControl>;
async fn after_create_user(
&self,
user: &S::User,
ctx: &SeaOrmHookContext<'_>,
) -> AuthResult<()>;
async fn before_update_user(
&self,
id: &str,
update: &mut UpdateUser,
ctx: &SeaOrmHookContext<'_>,
) -> AuthResult<HookControl>;
async fn after_update_user(
&self,
user: &S::User,
ctx: &SeaOrmHookContext<'_>,
) -> AuthResult<()>;
async fn before_delete_user(
&self,
user: &S::User,
ctx: &SeaOrmHookContext<'_>,
) -> AuthResult<HookControl>;
async fn after_delete_user(
&self,
user: &S::User,
ctx: &SeaOrmHookContext<'_>,
) -> AuthResult<()>;
async fn before_create_session(
&self,
session: &mut CreateSession,
ctx: &SeaOrmHookContext<'_>,
) -> AuthResult<HookControl>;
async fn after_create_session(
&self,
session: &S::Session,
ctx: &SeaOrmHookContext<'_>,
) -> AuthResult<()>;
}Rejecting Operations
Return HookControl::Cancel or an error from a before_* hook to abort the operation:
Atomic verification consumption returns None when a before-delete hook cancels. After-delete hooks run after the consumption transaction commits. An after-delete hook error propagates to the caller, but the consumed credential remains deleted.
#[async_trait]
impl SeaOrmHooks<AppAuthSchema> for BlockDisposableEmails {
async fn before_create_user(
&self,
user: &mut CreateUser,
_ctx: &SeaOrmHookContext<'_>,
) -> AuthResult<HookControl> {
if let Some(email) = &user.email {
if email.ends_with("@disposable.com") {
return Ok(HookControl::Cancel);
}
}
Ok(HookControl::Continue)
}
}Modifying Data
before_* hooks receive mutable references, so you can normalize or enrich values before they are persisted:
#[async_trait]
impl SeaOrmHooks<AppAuthSchema> for NormalizeEmail {
async fn before_create_user(
&self,
user: &mut CreateUser,
_ctx: &SeaOrmHookContext<'_>,
) -> AuthResult<HookControl> {
if let Some(email) = &mut user.email {
*email = email.to_lowercase();
}
Ok(HookControl::Continue)
}
}Provisioning After Signup
Use after_create_user to create application records. When ctx.tx is present, use that transaction for related writes. An after_* hook runs after the write, before the transaction commits. External side effects, such as sending email, do not roll back with the transaction.
use async_trait::async_trait;
use better_auth::{AuthResult, AuthSchema};
use better_auth::error::DatabaseError;
use better_auth::prelude::AuthUser;
use better_auth::seaorm::sea_orm::ConnectionTrait;
use better_auth::seaorm::sea_orm::sea_query::{Alias, Query};
use better_auth::seaorm::{SeaOrmHookContext, SeaOrmHooks, SeaOrmStore};
struct ProvisionWorkspaceHook;
#[async_trait]
impl SeaOrmHooks<AppAuthSchema> for ProvisionWorkspaceHook {
async fn after_create_user(
&self,
user: &<AppAuthSchema as AuthSchema>::User,
ctx: &SeaOrmHookContext<'_>,
) -> AuthResult<()> {
let statement = Query::insert()
.into_table(Alias::new("app_workspaces"))
.columns([Alias::new("user_id"), Alias::new("name")])
.values_panic([user.id().into_owned().into(), "Default Workspace".into()])
.to_owned();
let result = if let Some(tx) = ctx.tx {
tx.execute(&statement).await
} else {
ctx.db.execute(&statement).await
};
result.map_err(|error| DatabaseError::Query(error.to_string()))?;
Ok(())
}
}
let store = SeaOrmStore::<AppAuthSchema>::new(config.clone(), database)
.hook(ProvisionWorkspaceHook);Multiple Hooks
Multiple hooks can be registered. They execute in registration order:
let store = SeaOrmStore::<AppAuthSchema>::new(config.clone(), database)
.hook(AuditHook)
.hook(NormalizeEmail)
.hook(BlockDisposableEmails);
let auth = BetterAuth::<AppAuthSchema>::new(config)
.store(store)
.build()
.await?;If any before_* hook returns HookControl::Cancel or an error, subsequent hooks and the write itself are skipped.