Better Auth in Rust

Database Hooks

Intercept database operations with lifecycle hooks.

Database hooks let you run custom logic before and after user and session operations. Use them for logging, validation, side effects, or rejecting operations.

Setup

use better_auth::hooks::{DatabaseHookContext, DatabaseHooks, HookControl};
use async_trait::async_trait;

struct AuditHook;

#[async_trait]
impl DatabaseHooks for AuditHook {
    async fn after_create_user(
        &self,
        user: &better_auth::prelude::User,
        _ctx: &DatabaseHookContext<'_>,
    ) -> better_auth::error::AuthResult<()> {
        println!("User created: {}", user.email.as_deref().unwrap_or(""));
        Ok(())
    }
}

let auth = BetterAuth::new(config)
    .database(database)
    .database_hook(AuditHook)
    .build()
    .await?;

The DatabaseHooks Trait

All methods have default no-op implementations. Override only what you need.

#[async_trait]
pub trait DatabaseHooks: Send + Sync {
    // User hooks
    async fn before_create_user(
        &self,
        user: &mut CreateUser,
        ctx: &DatabaseHookContext<'_>,
    ) -> AuthResult<HookControl>;
    async fn after_create_user(
        &self,
        user: &User,
        ctx: &DatabaseHookContext<'_>,
    ) -> AuthResult<()>;
    async fn before_update_user(
        &self,
        id: &str,
        update: &mut UpdateUser,
        ctx: &DatabaseHookContext<'_>,
    ) -> AuthResult<HookControl>;
    async fn after_update_user(
        &self,
        user: &User,
        ctx: &DatabaseHookContext<'_>,
    ) -> AuthResult<()>;
    async fn before_delete_user(
        &self,
        user: &User,
        ctx: &DatabaseHookContext<'_>,
    ) -> AuthResult<HookControl>;
    async fn after_delete_user(
        &self,
        user: &User,
        ctx: &DatabaseHookContext<'_>,
    ) -> AuthResult<()>;

    // Session hooks
    async fn before_create_session(
        &self,
        session: &mut CreateSession,
        ctx: &DatabaseHookContext<'_>,
    ) -> AuthResult<HookControl>;
    async fn after_create_session(
        &self,
        session: &Session,
        ctx: &DatabaseHookContext<'_>,
    ) -> AuthResult<()>;
    async fn before_delete_session(
        &self,
        session: &Session,
        ctx: &DatabaseHookContext<'_>,
    ) -> AuthResult<HookControl>;
    async fn after_delete_session(
        &self,
        session: &Session,
        ctx: &DatabaseHookContext<'_>,
    ) -> AuthResult<()>;
}

Rejecting Operations

Return HookControl::Cancel or an error from a before_* hook to abort the operation:

#[async_trait]
impl DatabaseHooks for BlockDisposableEmails {
    async fn before_create_user(
        &self,
        user: &mut CreateUser,
        _ctx: &DatabaseHookContext<'_>,
    ) -> 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 modify the data before it's persisted:

#[async_trait]
impl DatabaseHooks for NormalizeEmail {
    async fn before_create_user(
        &self,
        user: &mut CreateUser,
        _ctx: &DatabaseHookContext<'_>,
    ) -> AuthResult<HookControl> {
        if let Some(email) = &mut user.email {
            *email = email.to_lowercase();
        }
        Ok(HookControl::Continue)
    }
}

Provisioning After Signup

For app-specific side effects such as creating a workspace, seeding records, or triggering background jobs, prefer after_create_user before reaching for a custom database adapter wrapper:

use async_trait::async_trait;
use better_auth::error::AuthResult;
use better_auth::hooks::{DatabaseHookContext, DatabaseHooks};
use better_auth::prelude::User;
use better_auth::store::DatabaseConnection;
use better_auth::store::sea_orm::{ConnectionTrait, Statement};

#[derive(Clone)]
struct ProvisionWorkspaceHook {
    db: DatabaseConnection,
}

#[async_trait]
impl DatabaseHooks for ProvisionWorkspaceHook {
    async fn after_create_user(
        &self,
        user: &User,
        _ctx: &DatabaseHookContext<'_>,
    ) -> AuthResult<()> {
        let sql = Statement::from_string(
            self.db.get_database_backend(),
            format!(
                "insert into app_workspaces (user_id, name) values ('{}', 'Default Workspace')",
                user.id
            ),
        );
        let _ = self.db.execute(sql).await?;
        Ok(())
    }
}

let auth = BetterAuth::new(config)
    .database(database.clone())
    .database_hook(ProvisionWorkspaceHook { db: database })
    .build()
    .await?;

Use a full adapter wrapper only when you need to replace the persistence behavior itself. For most onboarding and provisioning work, hooks are the cleaner integration point.

Multiple Hooks

Multiple hooks can be registered. They execute in registration order:

let auth = BetterAuth::new(config)
    .database(database)
    .database_hook(AuditHook)
    .database_hook(NormalizeEmail)
    .database_hook(BlockDisposableEmails)
    .build()
    .await?;

If any before_* hook returns HookControl::Cancel or an error, subsequent hooks and the operation itself are skipped.

On this page