| name | safe-action-better-auth |
| description | Use when adding authentication or authorization to safe actions with Better Auth -- covers betterAuth() middleware setup, typed session context (BetterAuthContext), custom authorize callbacks (AuthorizeFn), unauthorized() handling, nextCookies() configuration, and Next.js authInterrupts setup |
next-safe-action Better Auth Adapter
Install
npm install @next-safe-action/adapter-better-auth better-auth
Import
import { betterAuth } from "@next-safe-action/adapter-better-auth";
Quick Start
1. Set up Better Auth
Create your Better Auth server instance. Add the nextCookies() plugin if your actions need to set cookies (e.g. signInEmail, signUpEmail):
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
export const auth = betterAuth({
plugins: [
nextCookies(),
],
});
2. Enable auth interrupts in Next.js
The default behavior uses unauthorized() from next/navigation, which requires this flag:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
authInterrupts: true,
},
};
export default nextConfig;
3. Create an authenticated action client
import { createSafeActionClient } from "next-safe-action";
import { betterAuth } from "@next-safe-action/adapter-better-auth";
import { auth } from "./auth";
export const actionClient = createSafeActionClient();
export const authClient = actionClient.use(betterAuth(auth));
4. Use it in your actions
"use server";
import { z } from "zod";
import { authClient } from "@/lib/safe-action";
export const updateProfile = authClient
.inputSchema(z.object({ name: z.string().min(1) }))
.action(async ({ parsedInput, ctx }) => {
const userId = ctx.auth.user.id;
await db.user.update({
where: { id: userId },
data: { name: parsedInput.name },
});
return { success: true };
});
How It Works
betterAuth() creates a pre-validation middleware for the safe action client's .use() chain:
- Fetches the session by calling
auth.api.getSession({ headers: await headers() }) using the request headers from next/headers
- Blocks unauthenticated requests by calling
unauthorized() from next/navigation when no session exists
- Injects typed context by passing
{ auth: { user, session } } to next(), merging it into the action context
The context is namespaced under auth to avoid collisions with other middleware context properties.
Type Inference
The middleware infers the exact user and session types from your Better Auth instance, including any fields added by plugins. For example, if you use the organization plugin, ctx.auth.session will include activeOrganizationId. No manual type annotations are needed.
Entry Points
| Entry point | Exports | Environment |
|---|
@next-safe-action/adapter-better-auth | betterAuth, types | Server |
Exported Types
| Type | Description |
|---|
BetterAuthContext<O> | The context shape added by the middleware: { auth: { user, session } }. Types are inferred from the Better Auth instance via Auth<O>["$Infer"]["Session"]. |
AuthorizeFn<O, NC, Ctx> | The authorize callback signature. Receives { authData, ctx, next }. |
BetterAuthOpts<O, NC, Ctx> | The options object type for betterAuth(). Contains the optional authorize callback. |
vs. Manual Auth Middleware
If you are using Better Auth, prefer betterAuth(auth) over writing manual auth middleware. The adapter handles session fetching, cookie integration, typing, and unauthorized rejection automatically.
const authClient = actionClient.use(async ({ next }) => {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
throw new Error("Unauthorized");
}
return next({ ctx: { userId: session.user.id } });
});
const authClient = actionClient.use(betterAuth(auth));
Supporting Docs
Anti-Patterns
import { betterAuth } from "better-auth";
export const auth = betterAuth({
plugins: [
],
});
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
export const auth = betterAuth({
plugins: [
nextCookies(),
],
});
const nextConfig: NextConfig = {};
const nextConfig: NextConfig = {
experimental: {
authInterrupts: true,
},
};
actionClient.use(
betterAuth(auth, {
authorize: async ({ next }) => {
const session = await auth.api.getSession({ headers: await headers() });
if (!session || session.user.role !== "admin") {
unauthorized();
}
return next({ ctx: { auth: session } });
},
}),
);
actionClient.use(
betterAuth(auth, {
authorize: ({ authData, next }) => {
if (!authData || authData.user.role !== "admin") {
unauthorized();
}
return next({ ctx: { auth: authData } });
},
}),
);
import { auth } from "./auth";
const authClient = actionClient.use(async ({ next }) => {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) throw new Error("Unauthorized");
return next({ ctx: { user: session.user } });
});
import { betterAuth } from "@next-safe-action/adapter-better-auth";
import { auth } from "./auth";
const authClient = actionClient.use(betterAuth(auth));