Hooks
Hooks let you adjust Better Auth endpoint behavior without replacing the endpoint.
Before Hooks
Before hooks run before the endpoint handler.
Example: Enforce Email Domain Restriction
auth = BetterAuth.auth(
secret: ENV.fetch("BETTER_AUTH_SECRET"),
hooks: {
before: [
{
matcher: ->(ctx) { ctx.path == "/sign-up/email" },
handler: lambda do |ctx|
email = ctx.body["email"].to_s
if email.end_with?("@blocked.test")
BetterAuth::APIError.new("BAD_REQUEST", message: "Email domain is blocked")
end
end
}
]
}
)Return an APIError, Rack response, endpoint result, or hash response to stop the request early.
Example: Modify Request Context
hooks: {
before: [
{
matcher: ->(ctx) { ctx.path == "/sign-up/email" },
handler: lambda do |ctx|
ctx.merge_context!(
body: {
"name" => ctx.body["name"].to_s.strip
}
)
nil
end
}
]
}After Hooks
After hooks run after the endpoint handler and can replace or decorate the response.
Example: Send A Notification When A New User Is Registered
hooks: {
after: [
{
matcher: ->(ctx) { ctx.path == "/sign-up/email" },
handler: lambda do |ctx|
ctx.context.run_in_background(
-> { Analytics.track("signup", email: ctx.body["email"]) }
)
nil
end
}
]
}Ctx
Hook handlers receive BetterAuth::Endpoint::Context.
Request Response
JSON Responses
handler: ->(ctx) { ctx.json({blocked: true}, status: 403) }Redirects
handler: ->(ctx) { ctx.redirect("/login") }Cookies
handler: lambda do |ctx|
ctx.set_cookie("theme", "dark", same_site: "lax", path: "/")
nil
endRead request cookies with:
ctx.get_cookie("better-auth.session_token")Errors
handler: ->(ctx) { ctx.error("UNAUTHORIZED", message: "login required") }Context
ctx.context is the auth context. It exposes app options, cookies, adapters, configured social providers, secrets, logger, session config, and rate limit config.
New Session
ctx.context.new_session contains a newly created session when a route creates one.
Returned
Endpoint results are normalized into Rack responses after hooks run.
Response Headers
Use ctx.set_header to add headers:
ctx.set_header("x-auth-flow", "signup")Predefined Auth Cookies
Auth cookie definitions are available through:
ctx.context.auth_cookiesSecret
ctx.context.secretPassword
password = ctx.context.password
hash = password[:hash].call("password123")
password[:verify].call(password: "password123", hash: hash)Adapter
ctx.context.adapterInternal Adapter
Use the internal adapter to preserve database hooks and secondary storage semantics.
ctx.context.internal_adapter.find_user_by_email("john@example.com")GenerateId
The configured ID generator lives at:
ctx.context.options.advanced.dig(:database, :generate_id)RunInBackground
ctx.context.run_in_background(
-> { AuditLog.create!(event: "auth_event") }
)Configure the background task handler:
advanced: {
background_tasks: {
handler: ->(task) { Thread.new { task.call } }
}
}RunInBackgroundOrAwait
Ruby exposes one background entry point: run_in_background. If no handler is configured, the task is called inline.
Reusable Hooks
Extract hook definitions into methods or constants.
RequireCompanyEmail = {
matcher: ->(ctx) { ctx.path == "/sign-up/email" },
handler: lambda do |ctx|
next if ctx.body["email"].to_s.end_with?("@company.com")
ctx.error("BAD_REQUEST", message: "Use your company email")
end
}
auth = BetterAuth.auth(
secret: ENV.fetch("BETTER_AUTH_SECRET"),
hooks: {
before: [RequireCompanyEmail]
}
)