mcpField
B
better-auth

better-auth

github.com/jezweb安全1.1k434 次安装3 周前更新
代码仓库

关于 better-auth

面向应用开发者的 better-auth 实施指南,重点介绍 Cloudflare D1 配置、认证插件和常见配置错误的规避方法。

主要功能

  1. 01使用 Drizzle 或 Kysely 配置 D1
  2. 02OAuth 2.1 Provider 配置指南
  3. 03会话与 JWT 配置
  4. 04插件和框架集成说明
  5. 05速率限制模式

适用场景

  1. 01选择 D1 数据库适配器
  2. 02添加 OAuth Provider 能力
  3. 03配置无状态会话
  4. 04排查框架 Cookie 处理问题

MCP 相关

指南介绍了如何为 MCP 服务构建 OAuth 2.1 Provider,并指出专用 MCP 插件已弃用。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill better-auth

安装后在 Claude Code 中输入/better-auth确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
assets/
auth-flow-diagram.md
commands/
setup.md
references/
cloudflare-worker-drizzle.ts
cloudflare-worker-kysely.ts
database-schema.ts
hono-cloudflare.ts
react-client-hooks.tsx
references/nextjs/
README.md
postgres-example.ts
rules/
better-auth.md
scripts/
setup-d1-drizzle.sh

详细介绍

better-auth - D1 适配器与错误预防指南

软件包:[email protected](2026 年 1 月 21 日) 破坏性变更:仅支持 ESM(v1.4.0)、默认禁止管理员模拟(v1.4.6)、多团队表变更(v1.3)、D1 需要 Drizzle/Kysely(没有直接适配器)


⚠️ 重要:D1 适配器要求

better-auth 没有 d1Adapter()。你必须使用:

  • Drizzle ORM(推荐):drizzleAdapter(db, { provider: "sqlite" })
  • Kysely:new Kysely({ dialect: new D1Dialect({ database: env.DB }) })

详情请参阅下文的 Issue #1。


v1.4.10 新功能(2025 年 12 月 31 日)

主要功能:

  • OAuth 2.1 Provider 插件 - 构建自己的 OAuth Provider(取代 MCP 插件)
  • Patreon OAuth Provider - 使用 Patreon 社交登录
  • Kick OAuth Provider - 支持刷新令牌
  • Vercel OAuth Provider - 使用 Vercel 登录
  • 全局 backgroundTasks 配置 - 延迟执行操作,提升性能
  • 表单数据支持 - 电子邮件身份验证,并在缺少抓取元数据时回退
  • Stripe 增强 - 灵活的订阅生命周期、disableRedirect 选项

Admin 插件更新:

  • ⚠️ 破坏性变更:默认禁止模拟管理员(v1.4.6)
  • 支持基于权限更新用户的角色
  • 改进角色类型推断

安全修复:

  • 加固 SAML XML 解析器,并支持配置大小限制
  • 验证 SAML 断言时间戳,并支持按 Provider 设置时钟偏差
  • 对经 SSO 域名验证的 Provider 建立信任
  • 拒绝已弃用的算法
  • 强制要求行 nonce

📚 文档:https://www.better-auth.com/changelogs


v1.4.0 新功能(2025 年 11 月 22 日)

主要功能:

  • 无状态会话管理 - 无需数据库存储的会话
  • 仅支持 ESM 的软件包 ⚠️ 破坏性变更:不再支持 CommonJS
  • JWT 密钥轮换 - 自动轮换密钥,增强安全性
  • SCIM 配置 - 企业用户配置协议
  • @standard-schema/spec - 取代用于验证的 ZodType
  • CaptchaFox 集成 - 内置 CAPTCHA 支持
  • 自动检测服务端 IP
  • 基于 Cookie 的账号数据存储
  • 支持多个通行密钥来源
  • RP 发起的注销端点(OIDC)

📚 文档:https://www.better-auth.com/changelogs


v1.3 新功能(2025 年 7 月)

主要功能:

  • 基于 SAML 2.0 的 SSO - 企业单点登录(已移至单独的 @better-auth/sso 软件包)
  • 多团队支持 ⚠️ 破坏性变更:从成员表中移除 teamId,需要新增 teamMembers 表
  • 附加字段 - 为组织、成员和邀请模型添加自定义字段
  • 性能改进和错误修复

📚 文档:https://www.better-auth.com/blog/1-3


替代方案:Kysely 适配器模式

如果你更倾向于使用 Kysely 而不是 Drizzle:

文件:src/auth.ts

import { betterAuth } from "better-auth";
import { Kysely, CamelCasePlugin } from "kysely";
import { D1Dialect } from "kysely-d1";

type Env = {
  DB: D1Database;
  BETTER_AUTH_SECRET: string;
  // ... other env vars
};

export function createAuth(env: Env) {
  return betterAuth({
    secret: env.BETTER_AUTH_SECRET,

    // Kysely with D1Dialect
    database: {
      db: new Kysely({
        dialect: new D1Dialect({
          database: env.DB,
        }),
        plugins: [
          // CRITICAL: Required if using Drizzle schema with snake_case
          new CamelCasePlugin(),
        ],
      }),
      type: "sqlite",
    },

    emailAndPassword: {
      enabled: true,
    },

    // ... other config
  });
}

为什么使用 CamelCasePlugin?

如果你的 Drizzle 架构使用 snake_case 列名(例如 email_verified),而 better-auth 使用 camelCase(例如 emailVerified),CamelCasePlugin 会自动在两种格式之间进行转换。

⚠️ Cloudflare Workers 说明:D1 数据库绑定仅在请求处理程序(fetch() 函数)内部可用。你无法在请求上下文之外初始化 better-auth。请使用工厂函数模式:

// ❌ WRONG - DB binding not available outside request
const db = drizzle(env.DB, { schema }) // env.DB doesn't exist here
export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "sqlite" }) })

// ✅ CORRECT - Create auth instance per-request
export default {
  fetch(request, env, ctx) {
    const db = drizzle(env.DB, { schema })
    const auth = betterAuth({ database: drizzleAdapter(db, { provider: "sqlite" }) })
    return auth.handler(request)
  }
}

社区验证:多个生产环境实现确认了这一模式(Medium、AnswerOverflow、官方 Hono 示例)。


框架集成

TanStack Start

⚠️ 关键:TanStack Start 需要 reactStartCookies 插件才能正确处理 Cookie 设置。

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { reactStartCookies } from "better-auth/react-start";

export const auth = betterAuth({
  database: drizzleAdapter(db, { provider: "sqlite" }),
  plugins: [
    twoFactor(),
    organization(),
    reactStartCookies(), // ⚠️ MUST be LAST plugin
  ],
});

为什么需要它:TanStack Start 使用特殊的 Cookie 处理系统。没有此插件,signInEmail() 和 signUpEmail() 等身份验证函数将无法正确设置 Cookie,导致身份验证失败。

重要:reactStartCookies 插件必须是数组中的最后一个插件。

会话可空模式:在 TanStack Start 中使用 useSession() 时,会话对象始终存在,但未登录时 session.user 和 session.session 为 null:

const { data: session } = authClient.useSession()

// When NOT logged in:
console.log(session) // { user: null, session: null }
console.log(!!session) // true (unexpected!)

// Correct check:
if (session?.user) {
  // User is logged in
}

务必检查 session?.user 或 session?.session,不要只检查 session。这是预期行为(会话对象容器始终存在)。

API 路由设置(/src/routes/api/auth/$.ts):

import { auth } from '@/lib/auth'
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/api/auth/$')({
  server: {
    handlers: {
      GET: ({ request }) => auth.handler(request),
      POST: ({ request }) => auth.handler(request),
    },
  },
})

📚 官方文档:https://www.better-auth.com/docs/integrations/tanstack


可用插件(v1.4+)

Better Auth 提供用于高级身份验证功能的插件:

插件导入说明文档
OAuth 2.1 Providerbetter-auth/plugins构建支持 PKCE、JWT 令牌和授权流程的 OAuth 2.1 Provider(替代 MCP 和 OIDC 插件)📚
SSObetter-auth/plugins企业级单点登录,支持 OIDC、OAuth2 和 SAML 2.0📚
Stripebetter-auth/plugins支付和订阅管理,支持灵活的生命周期处理📚
MCPbetter-auth/plugins⚠️ 已弃用 - 请改用 OAuth 2.1 Provider📚
Expobetter-auth/expoReact Native/Expo,支持 webBrowserOptions 和上次登录方式跟踪📚
OAuth 2.1 Provider 插件(v1.4.9 新增)

为 MCP 服务、第三方应用或 API 访问构建自己的 OAuth Provider:

import { betterAuth } from "better-auth";
import { oauthProvider } from "better-auth/plugins";
import { jwt } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    jwt(), // Required for token signing
    oauthProvider({
      // Token expiration (seconds)
      accessTokenExpiresIn: 3600,      // 1 hour
      refreshTokenExpiresIn: 2592000,  // 30 days
      authorizationCodeExpiresIn: 600, // 10 minutes
    }),
  ],
});

主要功能:

  • 符合 OAuth 2.1 规范 - 必须使用 PKCE,仅支持 S256,不支持隐式流程
  • 三种授权类型:authorization_code、refresh_token、client_credentials
  • JWT 或不透明令牌 - 可配置令牌格式
  • 动态客户端注册 - 符合 RFC 7591
  • 授权管理 - 可跳过可信客户端的授权流程
  • OIDC UserInfo 端点 - /oauth2/userinfo,提供基于范围的声明

必需的 Well-Known 端点:

// app/api/.well-known/oauth-authorization-server/route.ts
export async function GET() {
  return Response.json({
    issuer: process.env.BETTER_AUTH_URL,
    authorization_endpoint: `${process.env.BETTER_AUTH_URL}/api/auth/oauth2/authorize`,
    token_endpoint: `${process.env.BETTER_AUTH_URL}/api/auth/oauth2/token`,
    // ... other metadata
  });
}

创建 OAuth 客户端:

const client = await auth.api.createOAuthClient({
  body: {
    name: "My MCP Server",
    redirectURLs: ["https://claude.ai/callback"],
    type: "public", // or "confidential"
  },
});
// Returns: { clientId, clientSecret (if confidential) }

📚 完整文档:https://www.better-auth.com/docs/plugins/oauth-provider

⚠️ 注意:此插件仍在积极开发中,目前可能尚不适合用于生产环境。


其他插件参考
插件说明文档
BearerAPI 令牌身份验证(用于 API 的 Cookie 替代方案)📚
One TapGoogle One Tap 便捷登录📚
SCIM企业用户预配(SCIM 2.0)📚
Anonymous无需个人身份信息即可访问的访客用户📚
Username基于用户名的登录(电子邮件登录的替代方式)📚
Generic OAuth支持 PKCE 的自定义 OAuth Provider📚
Multi-Session在同一浏览器中使用多个帐户📚
API Key支持速率限制的令牌身份验证📚
Bearer Token 插件

用于仅限 API 的身份验证(移动应用、命令行工具、第三方集成):

import { bearer } from "better-auth/plugins";
import { bearerClient } from "better-auth/client/plugins";

// Server
export const auth = betterAuth({
  plugins: [bearer()],
});

// Client - Store token after sign-in
const { token } = await authClient.signIn.email({ email, password });
localStorage.setItem("auth_token", token);

// Client - Configure fetch to include token
const authClient = createAuthClient({
  plugins: [bearerClient()],
  fetchOptions: {
    auth: { type: "Bearer", token: () => localStorage.getItem("auth_token") },
  },
});
Google One Tap 插件

为已登录 Google 的用户提供流畅的一键登录体验:

import { oneTap } from "better-auth/plugins";
import { oneTapClient } from "better-auth/client/plugins";

// Server
export const auth = betterAuth({
  plugins: [oneTap()],
});

// Client
authClient.oneTap({
  onSuccess: (session) => {
    window.location.href = "/dashboard";
  },
});

要求:在 Google Cloud Console 中配置授权的 JavaScript 来源。

Anonymous 插件

无需邮箱/密码即可访客访问:

import { anonymous } from "better-auth/plugins";

// Server
export const auth = betterAuth({
  plugins: [
    anonymous({
      emailDomainName: "anon.example.com", // temp@{id}.anon.example.com
      onLinkAccount: async ({ anonymousUser, newUser }) => {
        // Migrate anonymous user data to linked account
        await migrateUserData(anonymousUser.id, newUser.id);
      },
    }),
  ],
});

// Client
await authClient.signIn.anonymous();
// Later: user can link to real account via signIn.social/email
Generic OAuth 插件

添加内置列表中没有的自定义 OAuth 提供商:

import { genericOAuth } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    genericOAuth({
      config: [
        {
          providerId: "linear",
          clientId: env.LINEAR_CLIENT_ID,
          clientSecret: env.LINEAR_CLIENT_SECRET,
          discoveryUrl: "https://linear.app/.well-known/openid-configuration",
          scopes: ["openid", "email", "profile"],
          pkce: true, // Recommended
        },
      ],
    }),
  ],
});

回调 URL 格式:{baseURL}/api/auth/oauth2/callback/{providerId}


速率限制

内置速率限制,支持自定义规则:

export const auth = betterAuth({
  rateLimit: {
    window: 60,  // seconds (default: 60)
    max: 100,    // requests per window (default: 100)

    // Custom rules for sensitive endpoints
    customRules: {
      "/sign-in/email": { window: 10, max: 3 },
      "/two-factor/*": { window: 10, max: 3 },
      "/forget-password": { window: 60, max: 5 },
    },

    // Use Redis/KV for distributed systems
    storage: "secondary-storage", // or "database"
  },

  // Secondary storage for rate limiting
  secondaryStorage: {
    get: async (key) => env.KV.get(key),
    set: async (key, value, ttl) => env.KV.put(key, value, { expirationTtl: ttl }),
    delete: async (key) => env.KV.delete(key),
  },
});

注意:通过 auth.api.* 发起的服务端调用不受速率限制。


无状态会话(v1.4.0+)

将会话完全存储在签名 Cookie 中,无需数据库存储:

export const auth = betterAuth({
  session: {
    // Stateless: No database storage, session lives in cookie only
    storage: undefined, // or omit entirely

    // Cookie configuration
    cookieCache: {
      enabled: true,
      maxAge: 60 * 60 * 24 * 7, // 7 days
      encoding: "jwt", // Use JWT for stateless (not "compact")
    },

    // Session expiration
    expiresIn: 60 * 60 * 24 * 7, // 7 days
  },
});

适用场景:

存储类型使用场景权衡
无状态(仅 Cookie)读操作较多的应用、边缘/无服务器环境、无需撤销无法撤销会话,负载大小有限
D1 数据库完整的会话管理、审计跟踪、撤销最终一致性问题
KV 存储强一致性、高读取性能需要额外配置绑定

要点:

  • 无状态会话无法撤销(用户必须等待会话过期)
  • Cookie 大小限制约为 4KB(限制会话数据)
  • 使用 encoding: "jwt" 以实现互操作性,使用 "jwe" 进行加密
  • 所有实例上的服务端都必须使用一致的 BETTER_AUTH_SECRET

JWT 密钥轮换(v1.4.0+)

自动轮换 JWT 签名密钥,增强安全性:

import { jwt } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    jwt({
      // Key rotation (optional, enterprise security)
      keyRotation: {
        enabled: true,
        rotationInterval: 60 * 60 * 24 * 30, // Rotate every 30 days
        keepPreviousKeys: 3, // Keep 3 old keys for validation
      },

      // Custom signing algorithm (default: HS256)
      algorithm: "RS256", // Requires asymmetric keys

      // JWKS endpoint (auto-generated at /api/auth/jwks)
      exposeJWKS: true,
    }),
  ],
});

要点:

  • 密钥轮换可防止被攻破的密钥无限期有效
  • 旧密钥会暂时保留,用于验证现有令牌
  • 为外部服务提供位于 /api/auth/jwks 的 JWKS 端点
  • 微服务进行公钥验证时使用 RS256
  • 单服务应用使用 HS256(默认)

提供商权限范围参考

常见 OAuth 提供商及获取用户数据所需的权限范围:

提供商权限范围返回内容
Googleopenid仅用户 ID
email邮箱地址、email_verified
profile姓名、头像(图片)、区域设置
GitHubuser:email邮箱地址(可能为私密地址)
read:user姓名、头像、个人资料 URL、简介
Microsoftopenid仅用户 ID
email邮箱地址
profile姓名、区域设置
User.Read来自 Graph API 的完整个人资料
Discordidentify用户名、头像、区分符
email邮箱地址
Applename名字/姓氏(仅首次授权时)
email邮箱或转发地址
Patreonidentity用户 ID、姓名
identity[email]邮箱地址
Vercel(自动)邮箱、姓名、头像

配置示例:

socialProviders: {
  google: {
    clientId: env.GOOGLE_CLIENT_ID,
    clientSecret: env.GOOGLE_CLIENT_SECRET,
    scope: ["openid", "email", "profile"], // All user data
  },
  github: {
    clientId: env.GITHUB_CLIENT_ID,
    clientSecret: env.GITHUB_CLIENT_SECRET,
    scope: ["user:email", "read:user"], // Email + full profile
  },
  microsoft: {
    clientId: env.MS_CLIENT_ID,
    clientSecret: env.MS_CLIENT_SECRET,
    scope: ["openid", "email", "profile", "User.Read"],
  },
}

会话 Cookie 缓存

会话 Cookie 的三种编码策略:

策略格式使用场景
紧凑型(默认)Base64url + HMAC-SHA256最小、最快
JWT标准 JWT可互操作
JWEA256CBC-HS512 加密最安全
export const auth = betterAuth({
  session: {
    cookieCache: {
      enabled: true,
      maxAge: 300, // 5 minutes
      encoding: "compact", // or "jwt" or "jwe"
    },
    freshAge: 60 * 60 * 24, // 1 day - operations requiring fresh session
  },
});

新建会话:某些敏感操作要求会话是近期创建的。配置 freshAge 以控制此时间窗口。


新增社交提供商(v1.4.9+)

socialProviders: {
  // Patreon - Creator economy
  patreon: {
    clientId: env.PATREON_CLIENT_ID,
    clientSecret: env.PATREON_CLIENT_SECRET,
    scope: ["identity", "identity[email]"],
  },

  // Kick - Streaming platform (with refresh tokens)
  kick: {
    clientId: env.KICK_CLIENT_ID,
    clientSecret: env.KICK_CLIENT_SECRET,
  },

  // Vercel - Developer platform
  vercel: {
    clientId: env.VERCEL_CLIENT_ID,
    clientSecret: env.VERCEL_CLIENT_SECRET,
  },
}

Cloudflare Workers 要求

⚠️ 重要:Cloudflare Workers 需要支持 AsyncLocalStorage:

# wrangler.toml
compatibility_flags = ["nodejs_compat"]
# or for older Workers:
# compatibility_flags = ["nodejs_als"]

如果未启用此标志,better-auth 会因上下文相关错误而失败。


数据库钩子

在数据库操作期间执行自定义逻辑:

export const auth = betterAuth({
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          // Validate or modify before creation
          if (user.email?.endsWith("@blocked.com")) {
            throw new APIError("BAD_REQUEST", { message: "Email domain not allowed" });
          }
          return { data: { ...user, role: "member" } };
        },
        after: async (user, ctx) => {
          // Send welcome email, create related records, etc.
          await sendWelcomeEmail(user.email);
          await createDefaultWorkspace(user.id);
        },
      },
    },
    session: {
      create: {
        after: async (session, ctx) => {
          // Audit logging
          await auditLog.create({ action: "session_created", userId: session.userId });
        },
      },
    },
  },
});

可用钩子:user、session、account、verification 表的 create、update。


Expo/React Native 集成

完整的移动端集成模式:

// Client setup with secure storage
import { expoClient } from "@better-auth/expo";
import * as SecureStore from "expo-secure-store";

const authClient = createAuthClient({
  baseURL: "https://api.example.com",
  plugins: [expoClient({ storage: SecureStore })],
});

// OAuth with deep linking
await authClient.signIn.social({
  provider: "google",
  callbackURL: "myapp://auth/callback", // Deep link
});

// Or use ID token verification (no redirect)
await authClient.signIn.social({
  provider: "google",
  idToken: {
    token: googleIdToken,
    nonce: generatedNonce,
  },
});

// Authenticated requests
const cookie = await authClient.getCookie();
await fetch("https://api.example.com/data", {
  headers: { Cookie: cookie },
  credentials: "omit",
});

app.json 深层链接设置:

{
  "expo": {
    "scheme": "myapp"
  }
}

服务端 trustedOrigins(开发环境):

trustedOrigins: ["exp://**", "myapp://"]

API 参考

概览:免费获得的功能

调用 auth.handler() 时,better-auth 会自动在 /api/auth/* 下公开 80+ 个可用于生产环境的 REST 端点。每个端点也都可通过 auth.api.* 作为服务端方法使用,以便以编程方式调用。

这种双层 API 系统意味着:

  • 客户端(React、Vue、移动应用)可直接调用 HTTP 端点
  • 服务端代码(中间件、后台任务)使用 auth.api.* 方法
  • 零样板代码——无需手动编写身份验证端点

节省时间:从零构建约需 220 小时。使用 better-auth 约需 4–8 小时。减少 97%。


自动生成的 HTTP 端点

使用 auth.handler() 时,所有端点都会自动在 /api/auth/* 下公开。

核心身份验证端点
端点方法描述
/sign-up/emailPOST使用邮箱和密码注册
/sign-in/emailPOST使用邮箱和密码进行身份验证
/sign-outPOST用户登出
/change-passwordPOST更新密码(需要当前密码)
/forget-passwordPOST发起密码重置流程
/reset-passwordPOST使用令牌完成密码重置
/send-verification-emailPOST发送邮箱验证链接
/verify-emailGET使用令牌验证邮箱(?token=<token>)
/get-sessionGET获取当前会话
/list-sessionsGET获取用户的所有活跃会话
/revoke-sessionPOST结束指定会话
/revoke-other-sessionsPOST结束除当前会话外的所有会话
/revoke-sessionsPOST结束用户的所有会话
/update-userPOST修改用户资料(姓名、图片)
/change-emailPOST更新邮箱地址
/set-passwordPOST为仅使用 OAuth 的账户添加密码
/delete-userPOST删除用户账户
/list-accountsGET获取已关联的身份验证提供方
/link-socialPOST将 OAuth 提供方关联到账户
/unlink-accountPOST取消关联提供方
社交 OAuth 端点
端点方法描述
/sign-in/socialPOST发起 OAuth 流程(在请求体中指定提供方)
/callback/:providerGETOAuth 回调处理程序(例如 /callback/google)
/get-access-tokenGET获取提供方访问令牌

OAuth 流程示例:

// Client initiates
await authClient.signIn.social({
  provider: "google",
  callbackURL: "/dashboard",
});

// better-auth handles redirect to Google
// Google redirects back to /api/auth/callback/google
// better-auth creates session automatically

插件端点
双重身份验证(2FA 插件)
import { twoFactor } from "better-auth/plugins";
端点方法描述
/two-factor/enablePOST为用户启用 2FA
/two-factor/disablePOST停用 2FA
/two-factor/get-totp-uriGET获取身份验证器应用的二维码 URI
/two-factor/verify-totpPOST验证身份验证器中的 TOTP 代码
/two-factor/send-otpPOST通过邮箱发送 OTP
/two-factor/verify-otpPOST验证邮箱 OTP
/two-factor/generate-backup-codesPOST创建恢复代码
/two-factor/verify-backup-codePOST使用备用代码登录
/two-factor/view-backup-codesGET查看当前备用代码

📚 文档:https://www.better-auth.com/docs/plugins/2fa

Organization Plugin(多租户 SaaS)
import { organization } from "better-auth/plugins";

组织(10 个端点):

端点方法描述
/organization/createPOST创建组织
/organization/listGET列出用户的组织
/organization/get-fullGET获取完整的组织详情
/organization/updatePUT修改组织
/organization/deleteDELETE删除组织
/organization/check-slugGET检查 slug 是否可用
/organization/set-activePOST设置当前组织上下文

成员(8 个端点):

端点方法描述
/organization/list-membersGET获取组织成员
/organization/add-memberPOST直接添加成员
/organization/remove-memberDELETE移除成员
/organization/update-member-rolePUT更改成员角色
/organization/get-active-memberGET获取当前成员信息
/organization/leavePOST离开组织

邀请(7 个端点):

端点方法描述
/organization/invite-memberPOST发送邀请邮件
/organization/accept-invitationPOST接受邀请
/organization/reject-invitationPOST拒绝邀请
/organization/cancel-invitationPOST取消待处理的邀请
/organization/get-invitationGET获取邀请详情
/organization/list-invitationsGET列出组织的邀请
/organization/list-user-invitationsGET列出用户待处理的邀请

团队(8 个端点):

端点方法描述
/organization/create-teamPOST在组织内创建团队
/organization/list-teamsGET列出组织团队
/organization/update-teamPUT修改团队
/organization/remove-teamDELETE移除团队
/organization/set-active-teamPOST设置当前团队上下文
/organization/list-team-membersGET列出团队成员
/organization/add-team-memberPOST向团队添加成员
/organization/remove-team-memberDELETE移除团队成员

权限与角色(6 个端点):

端点方法描述
/organization/has-permissionPOST检查用户是否拥有权限
/organization/create-rolePOST创建自定义角色
/organization/delete-roleDELETE删除自定义角色
/organization/list-rolesGET列出所有角色
/organization/get-roleGET获取角色详情
/organization/update-rolePUT修改角色权限

📚 文档:https://www.better-auth.com/docs/plugins/organization

Admin Plugin
import { admin } from "better-auth/plugins";

// v1.4.10 configuration options
admin({
  defaultRole: "user",
  adminRoles: ["admin"],
  adminUserIds: ["user_abc123"], // Always grant admin to specific users
  impersonationSessionDuration: 3600, // 1 hour (seconds)
  allowImpersonatingAdmins: false, // ⚠️ Default changed in v1.4.6
  defaultBanReason: "Violation of Terms of Service",
  bannedUserMessage: "Your account has been suspended",
})
端点方法描述
/admin/create-userPOST以管理员身份创建用户
/admin/list-usersGET列出所有用户(带筛选条件/分页)
/admin/set-rolePOST为用户分配角色
/admin/set-user-passwordPOST更改用户密码
/admin/update-userPUT修改用户详情
/admin/remove-userDELETE删除用户账户
/admin/ban-userPOST封禁用户账户(可选设置到期时间)
/admin/unban-userPOST解封用户
/admin/list-user-sessionsGET获取用户的活动会话
/admin/revoke-user-sessionDELETE结束指定用户会话
/admin/revoke-user-sessionsDELETE结束用户的所有会话
/admin/impersonate-userPOST开始模拟用户身份
/admin/stop-impersonatingPOST结束模拟会话

⚠️ 破坏性变更(v1.4.6):allowImpersonatingAdmins 现在默认值为 false。如果需要管理员模拟管理员身份,请显式设置为 true。

带权限的自定义角色(v1.4.10):

import { createAccessControl } from "better-auth/plugins/access";

// Define resources and permissions
const ac = createAccessControl({
  user: ["create", "read", "update", "delete", "ban", "impersonate"],
  project: ["create", "read", "update", "delete", "share"],
} as const);

// Create custom roles
const supportRole = ac.newRole({
  user: ["read", "ban"],      // Can view and ban users
  project: ["read"],          // Can view projects
});

const managerRole = ac.newRole({
  user: ["read", "update"],
  project: ["create", "read", "update", "delete"],
});

// Use in plugin
admin({
  ac,
  roles: {
    support: supportRole,
    manager: managerRole,
  },
})

📚 文档:https://www.better-auth.com/docs/plugins/admin

其他插件端点

Passkey 插件(5 个端点)- 文档:

  • /passkey/add、/sign-in/passkey、/passkey/list、/passkey/delete、/passkey/update

Magic Link 插件(2 个端点)- 文档:

  • /sign-in/magic-link、/magic-link/verify

Username 插件(2 个端点)- 文档:

  • /sign-in/username、/username/is-available

Phone Number 插件(5 个端点)- 文档:

  • /sign-in/phone-number、/phone-number/send-otp、/phone-number/verify、/phone-number/request-password-reset、/phone-number/reset-password

Email OTP 插件(6 个端点)- 文档:

  • /email-otp/send-verification-otp、/email-otp/check-verification-otp、/sign-in/email-otp、/email-otp/verify-email、/forget-password/email-otp、/email-otp/reset-password

Anonymous 插件(1 个端点)- 文档:

  • /sign-in/anonymous

JWT 插件(2 个端点)- 文档:

  • /token(获取 JWT)、/jwks(用于验证的公钥)

OpenAPI 插件(2 个端点)- 文档:

  • /reference(使用 Scalar UI 的交互式 API 文档)
  • /generate-openapi-schema(以 JSON 格式获取 OpenAPI 规范)

服务端 API 方法(auth.api.*)

每个 HTTP 端点都有对应的服务端方法。可将这些方法用于:

  • 服务端中间件(保护路由)
  • 后台作业(清理用户、发送通知)
  • 管理操作(批量管理用户)
  • 自定义身份验证流程(以编程方式创建会话)
核心 API 方法
// Authentication
await auth.api.signUpEmail({
  body: { email, password, name },
  headers: request.headers,
});

await auth.api.signInEmail({
  body: { email, password, rememberMe: true },
  headers: request.headers,
});

await auth.api.signOut({ headers: request.headers });

// Session Management
const session = await auth.api.getSession({ headers: request.headers });

await auth.api.listSessions({ headers: request.headers });

await auth.api.revokeSession({
  body: { token: "session_token_here" },
  headers: request.headers,
});

// User Management
await auth.api.updateUser({
  body: { name: "New Name", image: "https://..." },
  headers: request.headers,
});

await auth.api.changeEmail({
  body: { newEmail: "[email protected]" },
  headers: request.headers,
});

await auth.api.deleteUser({
  body: { password: "current_password" },
  headers: request.headers,
});

// Account Linking
await auth.api.linkSocialAccount({
  body: { provider: "google" },
  headers: request.headers,
});

await auth.api.unlinkAccount({
  body: { providerId: "google", accountId: "google_123" },
  headers: request.headers,
});
插件 API 方法

2FA 插件:

// Enable 2FA
const { totpUri, backupCodes } = await auth.api.enableTwoFactor({
  body: { issuer: "MyApp" },
  headers: request.headers,
});

// Verify TOTP code
await auth.api.verifyTOTP({
  body: { code: "123456", trustDevice: true },
  headers: request.headers,
});

// Generate backup codes
const { backupCodes } = await auth.api.generateBackupCodes({
  headers: request.headers,
});

Organization 插件:

// Create organization
const org = await auth.api.createOrganization({
  body: { name: "Acme Corp", slug: "acme" },
  headers: request.headers,
});

// Add member
await auth.api.addMember({
  body: {
    userId: "user_123",
    role: "admin",
    organizationId: org.id,
  },
  headers: request.headers,
});

// Check permissions
const hasPermission = await auth.api.hasPermission({
  body: {
    organizationId: org.id,
    permission: "users:delete",
  },
  headers: request.headers,
});

Admin 插件:

// List users with pagination
const users = await auth.api.listUsers({
  query: {
    search: "john",
    limit: 10,
    offset: 0,
    sortBy: "createdAt",
    sortOrder: "desc",
  },
  headers: request.headers,
});

// Ban user
await auth.api.banUser({
  body: {
    userId: "user_123",
    reason: "Violation of ToS",
    expiresAt: new Date("2025-12-31"),
  },
  headers: request.headers,
});

// Impersonate user (for admin support)
const impersonationSession = await auth.api.impersonateUser({
  body: {
    userId: "user_123",
    expiresIn: 3600, // 1 hour
  },
  headers: request.headers,
});

如何选择
使用场景使用 HTTP 端点使用 auth.api.* 方法
客户端身份验证✅ 是❌ 否
服务端中间件❌ 否✅ 是
后台作业❌ 否✅ 是
管理面板✅ 是(来自客户端)✅ 是(来自服务端)
自定义身份验证流程❌ 否✅ 是
移动应用✅ 是❌ 否
API 路由✅ 是(代理到处理程序)✅ 是(直接调用)

示例:受保护路由中间件

import { Hono } from "hono";
import { createAuth } from "./auth";
import { createDatabase } from "./db";

const app = new Hono<{ Bindings: Env }>();

// Middleware using server-side API
app.use("/api/protected/*", async (c, next) => {
  const db = createDatabase(c.env.DB);
  const auth = createAuth(db, c.env);

  // Use server-side method
  const session = await auth.api.getSession({
    headers: c.req.raw.headers,
  });

  if (!session) {
    return c.json({ error: "Unauthorized" }, 401);
  }

  // Attach to context
  c.set("user", session.user);
  c.set("session", session.session);

  await next();
});

// Protected route
app.get("/api/protected/profile", async (c) => {
  const user = c.get("user");
  return c.json({ user });
});

发现可用端点

使用 OpenAPI 插件查看配置中的所有端点:

import { betterAuth } from "better-auth";
import { openAPI } from "better-auth/plugins";

export const auth = betterAuth({
  database: /* ... */,
  plugins: [
    openAPI(), // Adds /api/auth/reference endpoint
  ],
});

交互式文档:访问 http://localhost:8787/api/auth/reference

其中会显示一个 Scalar UI,包含:

  • ✅ 按功能分组的所有可用端点
  • ✅ 带类型的请求/响应架构
  • ✅ 试用功能(在浏览器中测试端点)
  • ✅ 身份验证要求
  • ✅ 多种语言的代码示例

以编程方式访问:

const schema = await auth.api.generateOpenAPISchema();
console.log(JSON.stringify(schema, null, 2));
// Returns full OpenAPI 3.0 spec

量化节省的时间

从头开始构建(手动实现):

  • 核心身份验证端点(注册、登录、OAuth、会话):40 小时
  • 邮箱验证和密码重置:10 小时
  • 2FA 系统(TOTP、备用代码、邮箱 OTP):20 小时
  • 组织(团队、邀请、RBAC):60 小时
  • 管理面板(用户管理、模拟用户身份):30 小时
  • 测试和调试:50 小时
  • 安全加固:20 小时

手动工作总量:约 220 小时(全职 5.5 周)

使用 better-auth:

  • 初始设置:2–4 小时
  • 自定义和样式调整:2–4 小时

使用 better-auth 的总耗时:4–8 小时

节省:约 97% 的开发时间


关键要点

better-auth 提供 80+ 个可用于生产环境的端点,涵盖:

  • ✅ 核心身份验证(20 个端点)
  • ✅ 双重身份验证和无密码登录(15 个端点)
  • ✅ 组织和团队(35 个端点)
  • ✅ 管理员和用户管理(15 个端点)
  • ✅ 社交 OAuth(自动配置回调)
  • ✅ OpenAPI 文档(交互式界面)

你无需编写任何端点代码。 只需配置功能并调用 auth.handler()。


已知问题与解决方案

问题 1:“d1Adapter is not exported” 错误

问题:代码显示 import { d1Adapter } from 'better-auth/adapters/d1',但实际上并不存在。

症状:出现 TypeScript 错误或关于缺少导出的运行时错误。

解决方案:改用 Drizzle 或 Kysely:

// ❌ WRONG - This doesn't exist
import { d1Adapter } from 'better-auth/adapters/d1'
database: d1Adapter(env.DB)

// ✅ CORRECT - Use Drizzle
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { drizzle } from 'drizzle-orm/d1'
const db = drizzle(env.DB, { schema })
database: drizzleAdapter(db, { provider: "sqlite" })

// ✅ CORRECT - Use Kysely
import { Kysely } from 'kysely'
import { D1Dialect } from 'kysely-d1'
database: {
  db: new Kysely({ dialect: new D1Dialect({ database: env.DB }) }),
  type: "sqlite"
}

来源:已通过 4 个使用 better-auth + D1 的生产代码仓库验证


问题 2:架构生成失败

问题:npx better-auth migrate 不会创建兼容 D1 的架构。

症状:迁移 SQL 语法错误,或无法与 D1 配合使用。

解决方案:使用 Drizzle Kit 生成迁移:

# Generate migration from Drizzle schema
npx drizzle-kit generate

# Apply to D1
wrangler d1 migrations apply my-app-db --remote

原因:Drizzle Kit 会生成兼容 SQLite 且可与 D1 配合使用的 SQL。


问题 3:“CamelCase”与“snake_case”列不匹配

问题:数据库中有 email_verified,但 better-auth 需要 emailVerified。

症状:会话读取失败,用户数据缺少字段。

⚠️ 严重(v1.4.10+):在 better-auth 适配器中使用 Kysely 的 CamelCasePlugin 会破坏联接解析。该插件会将 _joined_user_user_id 之类的联接键转换为 _joinedUserUserId,导致会话查询中的用户数据为 null。

Drizzle 解决方案:从一开始就使用 camelCase 定义架构(如示例所示)。

使用 CamelCasePlugin 的 Kysely 解决方案:为 better-auth 使用不带 CamelCasePlugin 的独立 Kysely 实例:

// DB for better-auth (no CamelCasePlugin)
const authDb = new Kysely({
  dialect: new D1Dialect({ database: env.DB }),
})

// DB for app queries (with CamelCasePlugin)
const appDb = new Kysely({
  dialect: new D1Dialect({ database: env.DB }),
  plugins: [new CamelCasePlugin()],
})

export const auth = betterAuth({
  database: { db: authDb, type: "sqlite" },
})

来源:GitHub Issue #7136


问题 4:D1 的最终一致性

问题:写入后立即读取会话时返回过期数据。

症状:用户登录后,getSession() 在下一个请求中返回 null。

解决方案:使用 Cloudflare KV 存储会话(强一致性):

import { betterAuth } from "better-auth";

export function createAuth(db: Database, env: Env) {
  return betterAuth({
    database: drizzleAdapter(db, { provider: "sqlite" }),
    session: {
      storage: {
        get: async (sessionId) => {
          const session = await env.SESSIONS_KV.get(sessionId);
          return session ? JSON.parse(session) : null;
        },
        set: async (sessionId, session, ttl) => {
          await env.SESSIONS_KV.put(sessionId, JSON.stringify(session), {
            expirationTtl: ttl,
          });
        },
        delete: async (sessionId) => {
          await env.SESSIONS_KV.delete(sessionId);
        },
      },
    },
  });
}

添加到 wrangler.toml:

[[kv_namespaces]]
binding = "SESSIONS_KV"
id = "your-kv-namespace-id"

问题 5:SPA 应用的 CORS 错误

问题:身份验证 API 与前端位于不同源时出现 CORS 错误。

症状:浏览器控制台中出现 Access-Control-Allow-Origin 错误。

解决方案:在 Worker 中配置 CORS 标头,并确保 trustedOrigins 匹配:

import { cors } from "hono/cors";

// CRITICAL: Both must match frontend origin exactly
app.use(
  "/api/auth/*",
  cors({
    origin: "http://localhost:5173", // Frontend URL (no trailing slash)
    credentials: true, // Allow cookies
    allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
  })
);

// And in better-auth config
export const auth = betterAuth({
  trustedOrigins: ["http://localhost:5173"], // Same as CORS origin
  // ...
});

常见错误:

  • 源 URL 输入错误(末尾多了斜杠、http 和 https 不一致、端口错误)
  • CORS 配置与 trustedOrigins 中的源不匹配
  • CORS 中间件在身份验证路由之后注册(必须在之前注册)

来源:GitHub Issue #7434


问题 6:OAuth 重定向 URI 不匹配

问题:社交登录失败,并出现“redirect_uri_mismatch”错误。

症状:用户授权后,Google/GitHub OAuth 返回错误。

解决方案:确保 OAuth 提供商设置中的内容完全匹配:

Provider setting: https://yourdomain.com/api/auth/callback/google
better-auth URL:  https://yourdomain.com/api/auth/callback/google

❌ Wrong: http vs https, trailing slash, subdomain mismatch
✅ Right: Exact character-for-character match

检查 better-auth 回调 URL:

// It's always: {baseURL}/api/auth/callback/{provider}
const callbackURL = `${env.BETTER_AUTH_URL}/api/auth/callback/google`;
console.log("Configure this URL in Google Console:", callbackURL);

问题 7:缺少依赖项

问题:出现 TypeScript 错误或关于缺少软件包的运行时错误。

症状:出现 Cannot find module 'drizzle-orm' 或类似错误。

解决方案:安装所有必需的软件包:

Drizzle 方案:

npm install better-auth drizzle-orm drizzle-kit @cloudflare/workers-types

Kysely 方案:

npm install better-auth kysely kysely-d1 @cloudflare/workers-types

问题 8:验证邮件未发送

问题:验证邮件链接始终无法送达。

症状:用户注册后未收到邮件。

解决方案:实现 sendVerificationEmail 处理程序:

export const auth = betterAuth({
  database: /* ... */,
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
  },
  emailVerification: {
    sendVerificationEmail: async ({ user, url }) => {
      // Use your email service (SendGrid, Resend, etc.)
      await sendEmail({
        to: user.email,
        subject: "Verify your email",
        html: `
          <p>Click the link below to verify your email:</p>
          <a href="${url}">Verify Email</a>
        `,
      });
    },
    sendOnSignUp: true,
    autoSignInAfterVerification: true,
    expiresIn: 3600, // 1 hour
  },
});

对于 Cloudflare:使用 Cloudflare Email Routing 或外部服务(Resend、SendGrid)。


问题 9:会话过期过快

问题:会话意外过期或永不过期。

症状:用户意外退出登录,或退出登录后会话仍然存在。

解决方案:配置会话过期时间:

export const auth = betterAuth({
  database: /* ... */,
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days (in seconds)
    updateAge: 60 * 60 * 24, // Update session every 24 hours
  },
});

问题 10:社交登录服务商缺少用户数据

问题:社交登录成功,但缺少用户数据(姓名、头像)。

症状:通过 Google/GitHub 登录后,session.user.name 为 null。

解决方案:请求额外的作用域:

socialProviders: {
  google: {
    clientId: env.GOOGLE_CLIENT_ID,
    clientSecret: env.GOOGLE_CLIENT_SECRET,
    scope: ["openid", "email", "profile"], // Include 'profile' for name/image
  },
  github: {
    clientId: env.GITHUB_CLIENT_ID,
    clientSecret: env.GITHUB_CLIENT_SECRET,
    scope: ["user:email", "read:user"], // 'read:user' for full profile
  },
}

问题 11:Drizzle Schema 的 TypeScript 错误

问题:TypeScript 抱怨 Schema 类型。

症状:Type 'DrizzleD1Database' is not assignable to...

解决方案:从数据库导出正确的类型:

// src/db/index.ts
import { drizzle, type DrizzleD1Database } from "drizzle-orm/d1";
import * as schema from "./schema";

export type Database = DrizzleD1Database<typeof schema>;

export function createDatabase(d1: D1Database): Database {
  return drizzle(d1, { schema });
}

问题 12:Wrangler 开发模式无法使用

问题:wrangler dev 因数据库错误而失败。

症状:本地开发时出现“找不到数据库”或迁移错误。

解决方案:先在本地应用迁移:

# Apply migrations to local D1
wrangler d1 migrations apply my-app-db --local

# Then run dev server
wrangler dev

问题 13:用户数据更新未反映在 UI 中(使用 TanStack Query 时)

问题:更新用户数据(例如头像、姓名)后,即使调用了 queryClient.invalidateQueries(),useSession() 中的更改也不会显示。

症状:成功更新后,头像图片或用户个人资料数据仍显示旧内容。TanStack Query 缓存显示了更新后的数据,但 better-auth 会话仍显示旧值。

根本原因:better-auth 使用 nanostores 管理会话状态,而不是 TanStack Query。调用 queryClient.invalidateQueries() 只会使 React Query 缓存失效,不会使 better-auth 的 nanostore 失效。

解决方案:更新用户数据后,手动通知 nanostore:

// Update user data
const { data, error } = await authClient.updateUser({
  image: newAvatarUrl,
  name: newName
})

if (!error) {
  // Manually invalidate better-auth session state
  authClient.$store.notify('$sessionSignal')

  // Optional: Also invalidate React Query if using it for other data
  queryClient.invalidateQueries({ queryKey: ['user-profile'] })
}

适用场景:

  • 同时使用 better-auth 和 TanStack Query
  • 更新用户个人资料字段(姓名、图片、电子邮件)
  • 任何在客户端修改会话用户数据的操作

替代方案:调用 useSession() 中的 refetch(),但 $store.notify() 更直接:

const { data: session, refetch } = authClient.useSession()
// After update
await refetch()

注意:$store 是未公开文档的内部 API。此模式已在生产环境中验证,但未来的 better-auth 版本可能会有所更改。

来源:社区发现的模式,已通过生产环境使用验证


问题 14:apiKey 表 Schema 与 D1 不匹配

问题:使用 D1 时,better-auth CLI(npx @better-auth/cli generate)因“Failed to initialize database adapter”而失败。

症状:CLI 无法连接到 D1 以检查 Schema。通过 CLI 运行迁移也无法正常工作。

根本原因:CLI 需要直接的 SQLite 连接,但 D1 需要使用 Cloudflare 的绑定 API。

解决方案:跳过 CLI,使用文档中的 apiKey Schema 手动创建迁移:

CREATE TABLE api_key (
  id TEXT PRIMARY KEY NOT NULL,
  user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
  name TEXT,
  start TEXT,
  prefix TEXT,
  key TEXT NOT NULL,
  enabled INTEGER DEFAULT 1,
  rate_limit_enabled INTEGER,
  rate_limit_time_window INTEGER,
  rate_limit_max INTEGER,
  request_count INTEGER DEFAULT 0,
  last_request INTEGER,
  remaining INTEGER,
  refill_interval INTEGER,
  refill_amount INTEGER,
  last_refill_at INTEGER,
  expires_at INTEGER,
  permissions TEXT,
  metadata TEXT,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL
);

要点:

  • 该表恰好有 21 列(截至 better-auth v1.4+)
  • 列名使用 snake_case(例如 rate_limit_time_window,而不是 rateLimitTimeWindow)
  • D1 不支持 ALTER TABLE DROP COLUMN——如果 Schema 出现偏差,请使用全新迁移模式(删除并重新创建表)
  • 在 Drizzle 适配器配置中,使用 apikey(小写)作为表名映射

D1 的全新迁移模式:

-- Drop in reverse dependency order
DROP TABLE IF EXISTS api_key;
DROP TABLE IF EXISTS session;
-- ... other tables

-- Recreate with clean schema
CREATE TABLE api_key (...);

来源:使用 D1 和 better-auth apiKey 插件进行生产环境调试


问题 15:Admin 插件要求数据库角色(双重授权)

问题:即使你的中间件检查通过,Admin 插件的方法(例如 listUsers)仍会报错“你无权列出用户”。

症状:自定义的 requireAdmin 中间件(检查 ADMIN_EMAILS 环境变量)检查通过,但 auth.api.listUsers() 返回 403。

根本原因:better-auth Admin 插件有 两个 授权层:

  1. 你的中间件 - 自定义检查(例如 ADMIN_EMAILS)
  2. better-auth 内部检查 - 检查数据库中的 user.role === 'admin'

两项检查都必须通过,Admin 插件的方法才能正常工作。

解决方案:在数据库中将用户角色设置为 'admin':

-- Fix for existing users
UPDATE user SET role = 'admin' WHERE email = '[email protected]';

或者在初始设置后,使用 Admin UI/API 设置角色。

原因:Admin 插件的 listUsers、banUser、impersonateUser 等方法都会检查数据库中的 user.role,而不是你的自定义中间件逻辑。

来源:生产环境调试——误导性的错误消息促使我们通过 wrangler tail 发现了根本原因


问题 16:Organization/Team 的 updated_at 必须允许为 null

问题:创建 Organization 时出现 SQL 约束错误,尽管 API 返回“slug 已存在”。

症状:

  • 错误消息显示“已存在使用此 slug 的 Organization”
  • 数据库表实际上为空
  • wrangler tail 显示:Failed query: insert into "organization" ... values (?, ?, ?, null, null, ?, null)

根本原因:better-auth 在创建时会插入 null 作为 updated_at(仅在更新时设置此字段)。如果你的 schema 有 NOT NULL 约束,插入就会失败。

解决方案:在 schema 和迁移中都将 updated_at 设为可空:

// Drizzle schema - CORRECT
export const organization = sqliteTable('organization', {
  // ...
  updatedAt: integer('updated_at', { mode: 'timestamp' }), // No .notNull()
});

export const team = sqliteTable('team', {
  // ...
  updatedAt: integer('updated_at', { mode: 'timestamp' }), // No .notNull()
});
-- Migration - CORRECT
CREATE TABLE organization (
  -- ...
  updated_at INTEGER  -- No NOT NULL
);

适用范围:organization 和 team 表(也可能包括其他插件表)

来源:生产环境调试——wrangler tail 揭示了“slug 已存在”误导性消息背后的实际 SQL 错误


问题 17:API 响应出现双重嵌套(listMembers 等)

问题:自定义 API 端点返回双重嵌套的数据,例如 { members: { members: [...], total: N } }。

症状:即使数据存在,UI 中的计数也显示“undefined”,列表为空。

根本原因:listMembers 等 better-auth 方法会返回 { members: [...], total: N }。使用 c.json({ members: result }) 包装会导致双重嵌套。

解决方案:从 better-auth 响应中提取数组:

// ❌ WRONG - Double nesting
const result = await auth.api.listMembers({ ... });
return c.json({ members: result });
// Returns: { members: { members: [...], total: N } }

// ✅ CORRECT - Extract array
const result = await auth.api.listMembers({ ... });
const members = result?.members || [];
return c.json({ members });
// Returns: { members: [...] }

受影响的方法(返回对象,而不是数组):

  • listMembers → { members: [...], total: N }
  • listUsers → { users: [...], total: N, limit: N }
  • listOrganizations → { organizations: [...] }(检查结构)
  • listInvitations → { invitations: [...] }

模式:将 better-auth 方法的返回类型包装到 API 响应中之前,务必先检查。

来源:生产环境调试——UI 显示的计数为“undefined”,检查 API 后发现了嵌套问题


问题 18:Expo 客户端 fromJSONSchema 崩溃(v1.4.16)

问题:从 @better-auth/expo/client 导入 expoClient 时,在 v1.4.16 上会因 TypeError: Cannot read property 'fromJSONSchema' of undefined 而崩溃。

症状:在 React Native/Expo 应用中导入 expoClient 后立即发生运行时崩溃。

根本原因:在 PR #6933(Expo 的基于 cookie 的 OAuth 状态修复)之后引入的回归。f4a9f15 之后的 3 个提交中有一个导致构建失败。

解决方案:

  • 临时方案:使用提交 f4a9f15 的持续构建版本(回归发生前)
  • 永久方案:等待修复(截至 2026-01-20,issue #7491 仍未解决)
// Crashes on v1.4.16
import { expoClient } from '@better-auth/expo/client'

// Workaround: Use continuous build at f4a9f15
// Or wait for fix in next release

来源:GitHub Issue #7491


问题 19:additionalFields string[] 返回字符串化的 JSON

问题:在 v1.4.12 之后,通过 Drizzle 直接查询时,type: 'string[]' 的 additionalFields 会返回字符串化的数组('["a","b"]'),而不是原生数组。

症状:user.notificationTokens 是字符串,而不是数组。依赖数组的代码会出错。

根本原因:在 Drizzle 适配器中,string[] 字段以 mode: 'json' 存储,该模式期望接收数组。但 better-auth v1.4.4+ 会向 Drizzle 传递字符串,导致二次字符串化。通过 Drizzle 直接查询时,值是字符串;但使用 better-auth internalAdapter 时,转换器会正确返回数组。

解决方案:

  1. 使用 better-auth internalAdapter,而不是直接查询 Drizzle(它带有转换器)
  2. 修改 Drizzle schema,将 string[] 字段设为 .jsonb()
  3. 手动解析 JSON 字符串,直到问题修复
// Config
additionalFields: {
  notificationTokens: {
    type: 'string[]',
    required: true,
    input: true,
  },
}

// Create user
notificationTokens: ['token1', 'token2']

// Result in DB (when querying via Drizzle directly)
// '["token1","token2"]' (string, not array)

来源:GitHub Issue #7440


问题 20:additionalFields 的 "returned" 属性会阻止输入

问题:在 additionalFields 上设置 returned: false 会导致字段无法通过 API 保存,即使设置了 input: true 也是如此。

症状:通过 API 端点创建或更新时,字段始终不会保存到数据库。

根本原因:returned: false 属性会同时阻止读取和写入操作,而不仅仅是预期的读取操作。input: true 属性应独立控制写入权限。

解决方案:

  • 如果需要通过 API 写入,请勿使用 returned: false
  • 改用服务端方法(auth.api.*)写入
// Organization plugin config
additionalFields: {
  secretField: {
    type: 'string',
    required: true,
    input: true,      // Should allow API writes
    returned: false,  // Should only block reads, but blocks writes too
  },
}

// API request to create organization
// secretField is never saved to database

来源:GitHub Issue #7489


问题 21:freshAge 基于创建时间,而非活动时间

问题:session.freshAge 检查的是距创建时间的时长,而不是近期活动。即使活跃会话一直在使用,经过 freshAge 指定的时间后也会变为“非新鲜”状态。

症状:要求“新鲜会话”的端点会拒绝有效的活跃会话。

原因:freshSessionMiddleware 会检查 Date.now() - (session.updatedAt || session.createdAt),但只有在根据 updateAge 刷新会话时,updatedAt 才会更新。如果 updateAge > freshAge,那么在 updatedAt 更新之前,会话就会变为“非新鲜”状态。

解决方案:

  1. 设置 updateAge <= freshAge,确保在过期前更新新鲜度
  2. 对长期会话,避免设置“要求新鲜会话”的门槛
  3. 接受这是设计如此:freshAge 严格基于距创建时间的时长(维护者已确认)
// Config
session: {
  expiresIn: 60 * 60 * 24 * 7,    // 7 days
  freshAge: 60 * 60 * 24,          // 24 hours
  updateAge: 60 * 60 * 24 * 3,     // 3 days (> freshAge!) ⚠️ PROBLEM

  // CORRECT - updateAge <= freshAge
  updateAge: 60 * 60 * 12,         // 12 hours (< freshAge)
}

// Timeline with bad config:
// T+0h: User signs in (createdAt = now)
// T+12h: User makes requests (session active, still fresh)
// T+25h: User makes request (session active, BUT NOT FRESH - freshAge elapsed)
// Result: "Fresh session required" endpoints reject active session

来源:GitHub Issue #7472


问题 22:OAuth 令牌端点返回包装后的 JSON

问题:OAuth 2.1 和 OIDC 令牌端点返回 { "response": { ...tokens... } },而不是符合规范的顶层 JSON。OAuth 客户端期望根级别包含 { "access_token": "...", "token_type": "bearer" }。

症状:OAuth 客户端报错 Bearer undefined 或 invalid_token。

根本原因:端点管道会为内部使用返回 { response, headers, status },该对象随后会直接序列化为 HTTP 响应。这违反了 OAuth/OIDC 规范要求。

解决方案:

  • 临时方案:在客户端手动解包 .response 字段
  • 永久方案:等待修复(issue #7355 尚未关闭,正在接受贡献)
// Expected (spec-compliant)
{ "access_token": "...", "token_type": "bearer", "expires_in": 3600 }

// Actual (wrapped)
{ "response": { "access_token": "...", "token_type": "bearer", "expires_in": 3600 } }

// Result: OAuth clients fail to parse, send `Bearer undefined`

来源:GitHub Issue #7355


迁移指南

从 Clerk 迁移

主要差异:

  • Clerk:第三方服务 → better-auth:可自托管
  • Clerk:专有 → better-auth:开源
  • Clerk:按月收费 → better-auth:免费

迁移步骤:

  1. 从 Clerk 导出用户数据(CSV 或 API)
  2. 导入 better-auth 数据库:
    // migration script
    const clerkUsers = await fetchClerkUsers();
    
    for (const clerkUser of clerkUsers) {
      await db.insert(user).values({
        id: clerkUser.id,
        email: clerkUser.email,
        emailVerified: clerkUser.email_verified,
        name: clerkUser.first_name + " " + clerkUser.last_name,
        image: clerkUser.profile_image_url,
      });
    }
    
  3. 将 Clerk SDK 替换为 better-auth 客户端:
    // Before (Clerk)
    import { useUser } from "@clerk/nextjs";
    const { user } = useUser();
    
    // After (better-auth)
    import { authClient } from "@/lib/auth-client";
    const { data: session } = authClient.useSession();
    const user = session?.user;
    
  4. 更新中间件以验证会话
  5. 配置社交服务提供商(使用相同的 OAuth 应用,但配置不同)

从 Auth.js(NextAuth)迁移

主要差异:

  • Auth.js:功能有限 → better-auth:功能全面(2FA、组织等)
  • Auth.js:依赖大量回调 → better-auth:基于插件
  • Auth.js:会话处理方式各异 → better-auth:处理方式一致

迁移步骤:

  1. 数据库架构:Auth.js 和 better-auth 使用相似的架构,但列名不同
  2. 替换配置:
    // Before (Auth.js)
    import NextAuth from "next-auth";
    import GoogleProvider from "next-auth/providers/google";
    
    export default NextAuth({
      providers: [GoogleProvider({ /* ... */ })],
    });
    
    // After (better-auth)
    import { betterAuth } from "better-auth";
    
    export const auth = betterAuth({
      socialProviders: {
        google: { /* ... */ },
      },
    });
    
  3. 更新客户端钩子:
    // Before
    import { useSession } from "next-auth/react";
    
    // After
    import { authClient } from "@/lib/auth-client";
    const { data: session } = authClient.useSession();
    

其他资源

官方文档
核心概念
身份验证方式
插件文档

核心插件:

无密码插件:

高级插件:

框架集成
社区与支持
相关文档

生产环境示例

已验证可运行的 D1 仓库(均使用 Drizzle 或 Kysely):

  1. zpg6/better-auth-cloudflare - Drizzle + D1(包含 CLI)
  2. zwily/example-react-router-cloudflare-d1-drizzle-better-auth - Drizzle + D1
  3. foxlau/react-router-v7-better-auth - Drizzle + D1
  4. matthewlynch/better-auth-react-router-cloudflare-d1 - Kysely + D1

没有任何一个使用直接的 d1Adapter - 都需要 Drizzle/Kysely。


版本兼容性

已测试版本:

破坏性变更:

  • v1.4.6:allowImpersonatingAdmins 默认值为 false
  • v1.4.0:仅支持 ESM(不支持 CommonJS)
  • v1.3.0:多团队表结构变更

查看更新日志:https://github.com/better-auth/better-auth/releases


社区资源

Cloudflare 专属指南:


Token 使用效率:

  • 不使用该技能:约 35,000 个 Token(D1 适配器错误、15+ 个插件、速率限制、会话缓存、数据库钩子、移动端集成)
  • 使用该技能:约 8,000 个 Token(聚焦于错误 + 模式 + 全部插件 + API 参考)
  • 节省:约 77%(约 27,000 个 Token)

预防的错误:22 个已记录的问题及其确切解决方案 核心价值:D1 适配器要求、nodejs_compat 标志、OAuth 2.1 提供方、Bearer/OneTap/SCIM/Anonymous 插件、速率限制、会话缓存、数据库钩子、Expo 集成、80+ 个端点参考、additionalFields 错误、freshAge 行为、OAuth Token 封装


最后验证时间:2026-01-21 | 技能版本:5.1.0 | 变更:根据训练截止日期之后的研究新增 5 个问题(Expo fromJSONSchema 崩溃、additionalFields string[] 错误、additionalFields 返回属性错误、freshAge 不基于活动时间、OAuth Token 封装)。扩展问题 #3,加入 Kysely CamelCasePlugin 联接解析失败。扩展问题 #5,加入 Hono CORS 模式。新增 Cloudflare Workers 数据库绑定限制说明。新增 TanStack Start 会话可空性模式。更新至 v1.4.16。

评论

支持 Markdown