safe-action-better-auth

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

next-safe-action Better Auth Adapter

next-safe-action Better Auth 适配器

Install

安装

bash
npm install @next-safe-action/adapter-better-auth better-auth
bash
npm install @next-safe-action/adapter-better-auth better-auth

Import

导入

ts
import { betterAuth } from "@next-safe-action/adapter-better-auth";
ts
import { betterAuth } from "@next-safe-action/adapter-better-auth";

Quick Start

快速开始

1. Set up Better Auth

1. 设置Better Auth

Create your Better Auth server instance. Add the
nextCookies()
plugin if your actions need to set cookies (e.g.
signInEmail
,
signUpEmail
):
ts
// 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
  ],
});
创建你的Better Auth服务器实例。如果你的操作需要设置Cookie(例如
signInEmail
signUpEmail
),请添加
nextCookies()
插件:
ts
// src/lib/auth.ts
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

2. 在Next.js中启用auth interrupts

The default behavior uses
unauthorized()
from
next/navigation
, which requires this flag:
ts
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  experimental: {
    authInterrupts: true,
  },
};

export default nextConfig;
默认行为使用
next/navigation
中的
unauthorized()
,这需要开启以下标识:
ts
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  experimental: {
    authInterrupts: true,
  },
};

export default nextConfig;

3. Create an authenticated action client

3. 创建已认证的操作客户端

ts
// 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));
ts
// src/lib/safe-action.ts
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

4. 在操作中使用

ts
// 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 };
  });
ts
// 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 和 ctx.auth.session 是完全类型化的,
    // 包括来自Better Auth插件的字段
    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:
  1. Fetches the session by calling
    auth.api.getSession({ headers: await headers() })
    using the request headers from
    next/headers
  2. Blocks unauthenticated requests by calling
    unauthorized()
    from
    next/navigation
    when no session exists
  3. 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.
betterAuth()
为安全操作客户端的
.use()
链创建一个预验证中间件:
  1. 获取会话:使用
    next/headers
    中的请求头调用`auth.api.getSession({ headers: await headers() })
  2. 拦截未认证请求:当不存在会话时,调用
    next/navigation
    中的
    unauthorized()
  3. 注入类型化上下文:将
    { auth: { user, session } }
    传递给
    next()
    ,合并到操作上下文中
上下文命名为
auth
命名空间,以避免与其他中间件上下文属性冲突。

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.
中间件会从你的Better Auth实例中推断出精确的
user
session
类型,包括插件添加的任何字段。例如,如果你使用
organization
插件,
ctx.auth.session
将包含
activeOrganizationId
。无需手动添加类型注解。

Entry Points

入口点

Entry pointExportsEnvironment
@next-safe-action/adapter-better-auth
betterAuth
, types
Server
入口点导出内容环境
@next-safe-action/adapter-better-auth
betterAuth
、类型
服务端

Exported Types

导出类型

TypeDescription
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.
类型描述
BetterAuthContext<O>
中间件添加的上下文结构:
{ auth: { user, session } }
。类型通过
Auth<O>["$Infer"]["Session"]
从Better Auth实例推断而来。
AuthorizeFn<O, NC, Ctx>
authorize
回调的签名。接收
{ authData, ctx, next }
BetterAuthOpts<O, NC, Ctx>
betterAuth()
的选项对象类型。包含可选的
authorize
回调。

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.
ts
// 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
如果你正在使用Better Auth,建议优先使用
betterAuth(auth)
而不是编写手动认证中间件。适配器会自动处理会话获取、Cookie集成、类型化和未授权拒绝。
ts
// 手动实现——如果已安装@next-safe-action/adapter-better-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: { userId: session.user.id } });
});

// 使用适配器——推荐这样做
const authClient = actionClient.use(betterAuth(auth));
// ctx.auth.user 和 ctx.auth.session 会自动完成全类型化

Supporting Docs

相关文档

  • Custom authorize patterns (role checks, redirects, org access)
  • 自定义授权模式(角色检查、重定向、组织访问)

Anti-Patterns

反模式

ts
// 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
  ],
});
ts
// 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,
  },
};
ts
// 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 } });
    },
  })
);
ts
// 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));
ts
// 错误:缺少nextCookies()插件——Server Actions中无法设置Cookie
// 当操作尝试设置Cookie时,会话会静默变为null
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  plugins: [/* 没有nextCookies() */],
});

// 正确:添加nextCookies()作为最后一个插件
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";

export const auth = betterAuth({
  plugins: [
    // ...其他插件
    nextCookies(), // 必须是最后一个
  ],
});
ts
// 错误:缺少authInterrupts标识——unauthorized()会抛出运行时错误
// next.config.ts
const nextConfig: NextConfig = {};

// 正确:启用authInterrupts
const nextConfig: NextConfig = {
  experimental: {
    authInterrupts: true,
  },
};
ts
// 错误:在authorize中重新获取会话——authData已经预获取
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 } });
    },
  })
);

// 正确:直接使用预获取的authData
actionClient.use(
  betterAuth(auth, {
    authorize: ({ authData, next }) => {
      if (!authData || authData.user.role !== "admin") {
        unauthorized();
      }
      return next({ ctx: { auth: authData } });
    },
  })
);
ts
// 错误:已安装适配器时仍编写手动Better Auth中间件
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 } });
});

// 正确:使用适配器——自动处理类型化、Cookie和unauthorized()
import { betterAuth } from "@next-safe-action/adapter-better-auth";
import { auth } from "./auth";

const authClient = actionClient.use(betterAuth(auth));