docs/content/docs/concepts/hooks.mdx
Hooks in Better Auth let you "hook into" the lifecycle and execute custom logic. They provide a way to customize Better Auth's behavior without writing a full plugin.
<Callout> We highly recommend using hooks if you need to make custom adjustments to an endpoint rather than making another endpoint outside of Better Auth. </Callout>Before hooks run before an endpoint is executed. Use them to modify requests, pre validate data, or return early.
This hook ensures that users can only sign up if their email ends with @example.com:
import { betterAuth } from "better-auth";
import { createAuthMiddleware, APIError } from "better-auth/api";
export const auth = betterAuth({
hooks: {
before: createAuthMiddleware(async (ctx) => {
if (ctx.path !== "/sign-up/email") {
return;
}
if (!ctx.body?.email.endsWith("@example.com")) {
throw new APIError("BAD_REQUEST", {
message: "Email must end with @example.com",
});
}
}),
},
});
To adjust the request context before proceeding:
import { betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";
export const auth = betterAuth({
hooks: {
before: createAuthMiddleware(async (ctx) => {
if (ctx.path === "/sign-up/email") {
return {
context: {
...ctx,
body: {
...ctx.body,
name: "John Doe",
},
}
};
}
}),
},
});
After hooks run after an endpoint is executed. Use them to modify responses.
import { betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";
import { sendMessage } from "@/lib/notification"
export const auth = betterAuth({
hooks: {
after: createAuthMiddleware(async (ctx) => {
if(ctx.path.startsWith("/sign-up")){
const newSession = ctx.context.newSession;
if(newSession){
sendMessage({
type: "user-register",
name: newSession.user.name,
})
}
}
}),
},
});
Since before and after each accept a single createAuthMiddleware call, use conditional checks on ctx.path to handle multiple endpoints within the same hook:
import { betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";
export const auth = betterAuth({
hooks: {
after: createAuthMiddleware(async (ctx) => {
if (ctx.path === "/reset-password") {
// Auto-login user after password reset
}
if (ctx.path.startsWith("/sign-up")) {
// Send welcome notification after signup
}
if (ctx.path === "/sign-in/email") {
// Track login analytics
}
}),
},
});
When you call createAuthMiddleware a ctx object is passed that provides a lot of useful properties. Including:
ctx.path to get the current endpoint path.ctx.body for parsed request body (available for POST requests).ctx.headers to access request headers.ctx.request to access the request object (may not exist in server-only endpoints).ctx.query to access query parameters.ctx.context auth related context, useful for accessing new session, auth cookies configuration, password hashing, config...and more.
This utilities allows you to get request information and to send response from a hook.
Use ctx.json to send JSON responses:
import { createAuthMiddleware } from "better-auth/api";
const hook = createAuthMiddleware(async (ctx) => {
return ctx.json({
message: "Hello World",
});
});
Use ctx.redirect to redirect users:
import { createAuthMiddleware } from "better-auth/api";
const hook = createAuthMiddleware(async (ctx) => {
throw ctx.redirect("/sign-up/name");
});
ctx.setCookie or ctx.setSignedCookie.ctx.getCookie or ctx.getSignedCookie.Example:
import { createAuthMiddleware } from "better-auth/api";
const hook = createAuthMiddleware(async (ctx) => {
ctx.setCookie("my-cookie", "value");
await ctx.setSignedCookie("my-signed-cookie", "value", ctx.context.secret, {
maxAge: 1000,
});
const cookie = ctx.getCookie("my-cookie");
const signedCookie = await ctx.getSignedCookie("my-signed-cookie", ctx.context.secret);
});
Throw errors with APIError for a specific status code and message:
import { createAuthMiddleware, APIError } from "better-auth/api";
const hook = createAuthMiddleware(async (ctx) => {
throw new APIError("BAD_REQUEST", {
message: "Invalid request",
});
});
The ctx object contains another context object inside that's meant to hold contexts related to auth. Including a newly created session on after hook, cookies configuration, password hasher and so on.
The newly created session after an endpoint is run. This only exist in after hook.
import { createAuthMiddleware } from "better-auth/api";
createAuthMiddleware(async (ctx) => {
const newSession = ctx.context.newSession
});
The returned value from the hook is passed to the next hook in the chain.
import { createAuthMiddleware } from "better-auth/api";
createAuthMiddleware(async (ctx) => {
const returned = ctx.context.returned; //this could be a successful response or an APIError
});
The response headers added by endpoints and hooks that run before this hook.
import { createAuthMiddleware } from "better-auth/api";
createAuthMiddleware(async (ctx) => {
const responseHeaders = ctx.context.responseHeaders;
});
Access BetterAuth’s predefined cookie properties:
import { createAuthMiddleware } from "better-auth/api";
createAuthMiddleware(async (ctx) => {
const cookieName = ctx.context.authCookies.sessionToken.name;
});
You can access the secret for your auth instance on ctx.context.secret
The password object provider hash and verify
ctx.context.password.hash: let's you hash a given password.ctx.context.password.verify: let's you verify given password and a hash.Adapter exposes the adapter methods used by Better Auth. Including findOne, findMany, create, delete, update and updateMany. You generally should use your actually db instance from your orm rather than this adapter.
These are calls to your db that perform specific actions. createUser, createSession, updateSession...
This may be useful to use instead of using your db directly to get access to databaseHooks, proper secondaryStorage support and so on. If you're make a query similar to what exist in this internal adapter actions it's worth a look.
You can use ctx.context.generateId to generate Id for various reasons.
Schedules a task to run after the response is sent. Use for fire-and-forget operations (cleanup, analytics, rate limit counter updates). Configure the handler in advanced.backgroundTasks.
import { betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";
export const auth = betterAuth({
hooks: {
after: createAuthMiddleware(async (ctx) => {
if (ctx.path.startsWith("/sign-up")) {
const newSession = ctx.context.newSession;
if (newSession) {
ctx.context.runInBackground(sendAnalyticsEvent(newSession.user.id));
}
}
}),
},
});
Defers the task when a handler is configured, otherwise awaits it. Use for operations that must complete (e.g. sending emails) but benefit from not blocking when a handler exists. Configure the handler in advanced.backgroundTasks.
import { betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";
export const auth = betterAuth({
hooks: {
after: createAuthMiddleware(async (ctx) => {
if (ctx.path.startsWith("/sign-up")) {
const newSession = ctx.context.newSession;
if (newSession) {
await ctx.context.runInBackgroundOrAwait(
sendWelcomeEmail(newSession.user)
);
}
}
}),
},
});
If you need to reuse a hook across multiple endpoints, consider creating a plugin. Learn more in the Plugins Documentation.