Loading...
Loading...
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
npx skill4agent add next-safe-action/skills safe-action-better-authnpm install @next-safe-action/adapter-better-auth better-authimport { betterAuth } from "@next-safe-action/adapter-better-auth";nextCookies()signInEmailsignUpEmail// src/lib/auth.ts
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
export const auth = betterAuth({
// ...your config (database, plugins, etc.)
plugins: [
// ...other plugins
nextCookies(), // must be the last plugin in the array
],
});unauthorized()next/navigation// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
authInterrupts: true,
},
};
export default nextConfig;// src/lib/safe-action.ts
import { createSafeActionClient } from "next-safe-action";
import { betterAuth } from "@next-safe-action/adapter-better-auth";
import { auth } from "./auth";
// Public action client (no auth required)
export const actionClient = createSafeActionClient();
// Authenticated action client
export const authClient = actionClient.use(betterAuth(auth));// src/app/actions.ts
"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 }) => {
// ctx.auth.user and ctx.auth.session are fully typed,
// including fields from Better Auth plugins
const userId = ctx.auth.user.id;
await db.user.update({
where: { id: userId },
data: { name: parsedInput.name },
});
return { success: true };
});betterAuth().use()auth.api.getSession({ headers: await headers() })next/headersunauthorized()next/navigation{ auth: { user, session } }next()authusersessionorganizationctx.auth.sessionactiveOrganizationId| Entry point | Exports | Environment |
|---|---|---|
| | Server |
| Type | Description |
|---|---|
| The context shape added by the middleware: |
| The |
| The options object type for |
betterAuth(auth)// Manual — don't do this if you have @next-safe-action/adapter-better-auth installed
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 } });
});
// With adapter — do this instead
const authClient = actionClient.use(betterAuth(auth));
// ctx.auth.user and ctx.auth.session are fully typed automatically// BAD: Missing nextCookies() plugin — cookies won't be set in Server Actions
// Session will silently be null when actions try to set cookies
import { betterAuth } from "better-auth";
export const auth = betterAuth({
plugins: [/* no nextCookies() */],
});
// GOOD: Add nextCookies() as the last plugin
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
export const auth = betterAuth({
plugins: [
// ...other plugins
nextCookies(), // must be last
],
});// BAD: Missing authInterrupts flag — unauthorized() will throw a runtime error
// next.config.ts
const nextConfig: NextConfig = {};
// GOOD: Enable authInterrupts
const nextConfig: NextConfig = {
experimental: {
authInterrupts: true,
},
};// BAD: Re-fetching session inside authorize — it's already pre-fetched as authData
actionClient.use(
betterAuth(auth, {
authorize: async ({ next }) => {
const session = await auth.api.getSession({ headers: await headers() }); // Redundant!
if (!session || session.user.role !== "admin") {
unauthorized();
}
return next({ ctx: { auth: session } });
},
})
);
// GOOD: Use the pre-fetched authData directly
actionClient.use(
betterAuth(auth, {
authorize: ({ authData, next }) => {
if (!authData || authData.user.role !== "admin") {
unauthorized();
}
return next({ ctx: { auth: authData } });
},
})
);// BAD: Writing manual Better Auth middleware when the adapter is installed
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 } });
});
// GOOD: Use the adapter — handles typing, cookies, and unauthorized() automatically
import { betterAuth } from "@next-safe-action/adapter-better-auth";
import { auth } from "./auth";
const authClient = actionClient.use(betterAuth(auth));