关于 better-auth
面向应用开发者的 better-auth 实施指南,重点介绍 Cloudflare D1 配置、认证插件和常见配置错误的规避方法。
主要功能
- 01使用 Drizzle 或 Kysely 配置 D1
- 02OAuth 2.1 Provider 配置指南
- 03会话与 JWT 配置
- 04插件和框架集成说明
- 05速率限制模式
适用场景
- 01选择 D1 数据库适配器
- 02添加 OAuth Provider 能力
- 03配置无状态会话
- 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.jsonassets/auth-flow-diagram.mdcommands/setup.mdreferences/cloudflare-worker-drizzle.tscloudflare-worker-kysely.tsdatabase-schema.tshono-cloudflare.tsreact-client-hooks.tsxreferences/nextjs/README.mdpostgres-example.tsrules/better-auth.mdscripts/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 Provider | better-auth/plugins | 构建支持 PKCE、JWT 令牌和授权流程的 OAuth 2.1 Provider(替代 MCP 和 OIDC 插件) | 📚 |
| SSO | better-auth/plugins | 企业级单点登录,支持 OIDC、OAuth2 和 SAML 2.0 | 📚 |
| Stripe | better-auth/plugins | 支付和订阅管理,支持灵活的生命周期处理 | 📚 |
| MCP | better-auth/plugins | ⚠️ 已弃用 - 请改用 OAuth 2.1 Provider | 📚 |
| Expo | better-auth/expo | React 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
⚠️ 注意:此插件仍在积极开发中,目前可能尚不适合用于生产环境。
其他插件参考
| 插件 | 说明 | 文档 |
|---|---|---|
| Bearer | API 令牌身份验证(用于 API 的 Cookie 替代方案) | 📚 |
| One Tap | Google 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 提供商及获取用户数据所需的权限范围:
| 提供商 | 权限范围 | 返回内容 |
|---|---|---|
openid | 仅用户 ID | |
email | 邮箱地址、email_verified | |
profile | 姓名、头像(图片)、区域设置 | |
| GitHub | user:email | 邮箱地址(可能为私密地址) |
read:user | 姓名、头像、个人资料 URL、简介 | |
| Microsoft | openid | 仅用户 ID |
email | 邮箱地址 | |
profile | 姓名、区域设置 | |
User.Read | 来自 Graph API 的完整个人资料 | |
| Discord | identify | 用户名、头像、区分符 |
email | 邮箱地址 | |
| Apple | name | 名字/姓氏(仅首次授权时) |
email | 邮箱或转发地址 | |
| Patreon | identity | 用户 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 | 可互操作 |
| JWE | A256CBC-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/email | POST | 使用邮箱和密码注册 |
/sign-in/email | POST | 使用邮箱和密码进行身份验证 |
/sign-out | POST | 用户登出 |
/change-password | POST | 更新密码(需要当前密码) |
/forget-password | POST | 发起密码重置流程 |
/reset-password | POST | 使用令牌完成密码重置 |
/send-verification-email | POST | 发送邮箱验证链接 |
/verify-email | GET | 使用令牌验证邮箱(?token=<token>) |
/get-session | GET | 获取当前会话 |
/list-sessions | GET | 获取用户的所有活跃会话 |
/revoke-session | POST | 结束指定会话 |
/revoke-other-sessions | POST | 结束除当前会话外的所有会话 |
/revoke-sessions | POST | 结束用户的所有会话 |
/update-user | POST | 修改用户资料(姓名、图片) |
/change-email | POST | 更新邮箱地址 |
/set-password | POST | 为仅使用 OAuth 的账户添加密码 |
/delete-user | POST | 删除用户账户 |
/list-accounts | GET | 获取已关联的身份验证提供方 |
/link-social | POST | 将 OAuth 提供方关联到账户 |
/unlink-account | POST | 取消关联提供方 |
社交 OAuth 端点
| 端点 | 方法 | 描述 |
|---|---|---|
/sign-in/social | POST | 发起 OAuth 流程(在请求体中指定提供方) |
/callback/:provider | GET | OAuth 回调处理程序(例如 /callback/google) |
/get-access-token | GET | 获取提供方访问令牌 |
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/enable | POST | 为用户启用 2FA |
/two-factor/disable | POST | 停用 2FA |
/two-factor/get-totp-uri | GET | 获取身份验证器应用的二维码 URI |
/two-factor/verify-totp | POST | 验证身份验证器中的 TOTP 代码 |
/two-factor/send-otp | POST | 通过邮箱发送 OTP |
/two-factor/verify-otp | POST | 验证邮箱 OTP |
/two-factor/generate-backup-codes | POST | 创建恢复代码 |
/two-factor/verify-backup-code | POST | 使用备用代码登录 |
/two-factor/view-backup-codes | GET | 查看当前备用代码 |
📚 文档:https://www.better-auth.com/docs/plugins/2fa
Organization Plugin(多租户 SaaS)
import { organization } from "better-auth/plugins";
组织(10 个端点):
| 端点 | 方法 | 描述 |
|---|---|---|
/organization/create | POST | 创建组织 |
/organization/list | GET | 列出用户的组织 |
/organization/get-full | GET | 获取完整的组织详情 |
/organization/update | PUT | 修改组织 |
/organization/delete | DELETE | 删除组织 |
/organization/check-slug | GET | 检查 slug 是否可用 |
/organization/set-active | POST | 设置当前组织上下文 |
成员(8 个端点):
| 端点 | 方法 | 描述 |
|---|---|---|
/organization/list-members | GET | 获取组织成员 |
/organization/add-member | POST | 直接添加成员 |
/organization/remove-member | DELETE | 移除成员 |
/organization/update-member-role | PUT | 更改成员角色 |
/organization/get-active-member | GET | 获取当前成员信息 |
/organization/leave | POST | 离开组织 |
邀请(7 个端点):
| 端点 | 方法 | 描述 |
|---|---|---|
/organization/invite-member | POST | 发送邀请邮件 |
/organization/accept-invitation | POST | 接受邀请 |
/organization/reject-invitation | POST | 拒绝邀请 |
/organization/cancel-invitation | POST | 取消待处理的邀请 |
/organization/get-invitation | GET | 获取邀请详情 |
/organization/list-invitations | GET | 列出组织的邀请 |
/organization/list-user-invitations | GET | 列出用户待处理的邀请 |
团队(8 个端点):
| 端点 | 方法 | 描述 |
|---|---|---|
/organization/create-team | POST | 在组织内创建团队 |
/organization/list-teams | GET | 列出组织团队 |
/organization/update-team | PUT | 修改团队 |
/organization/remove-team | DELETE | 移除团队 |
/organization/set-active-team | POST | 设置当前团队上下文 |
/organization/list-team-members | GET | 列出团队成员 |
/organization/add-team-member | POST | 向团队添加成员 |
/organization/remove-team-member | DELETE | 移除团队成员 |
权限与角色(6 个端点):
| 端点 | 方法 | 描述 |
|---|---|---|
/organization/has-permission | POST | 检查用户是否拥有权限 |
/organization/create-role | POST | 创建自定义角色 |
/organization/delete-role | DELETE | 删除自定义角色 |
/organization/list-roles | GET | 列出所有角色 |
/organization/get-role | GET | 获取角色详情 |
/organization/update-role | PUT | 修改角色权限 |
📚 文档: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-user | POST | 以管理员身份创建用户 |
/admin/list-users | GET | 列出所有用户(带筛选条件/分页) |
/admin/set-role | POST | 为用户分配角色 |
/admin/set-user-password | POST | 更改用户密码 |
/admin/update-user | PUT | 修改用户详情 |
/admin/remove-user | DELETE | 删除用户账户 |
/admin/ban-user | POST | 封禁用户账户(可选设置到期时间) |
/admin/unban-user | POST | 解封用户 |
/admin/list-user-sessions | GET | 获取用户的活动会话 |
/admin/revoke-user-session | DELETE | 结束指定用户会话 |
/admin/revoke-user-sessions | DELETE | 结束用户的所有会话 |
/admin/impersonate-user | POST | 开始模拟用户身份 |
/admin/stop-impersonating | POST | 结束模拟会话 |
⚠️ 破坏性变更(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" },
})
问题 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 中间件在身份验证路由之后注册(必须在之前注册)
问题 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 插件有 两个 授权层:
- 你的中间件 - 自定义检查(例如 ADMIN_EMAILS)
- 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
问题 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 时,转换器会正确返回数组。
解决方案:
- 使用 better-auth
internalAdapter,而不是直接查询 Drizzle(它带有转换器) - 修改 Drizzle schema,将 string[] 字段设为
.jsonb() - 手动解析 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)
问题 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
问题 21:freshAge 基于创建时间,而非活动时间
问题:session.freshAge 检查的是距创建时间的时长,而不是近期活动。即使活跃会话一直在使用,经过 freshAge 指定的时间后也会变为“非新鲜”状态。
症状:要求“新鲜会话”的端点会拒绝有效的活跃会话。
原因:freshSessionMiddleware 会检查 Date.now() - (session.updatedAt || session.createdAt),但只有在根据 updateAge 刷新会话时,updatedAt 才会更新。如果 updateAge > freshAge,那么在 updatedAt 更新之前,会话就会变为“非新鲜”状态。
解决方案:
- 设置
updateAge <= freshAge,确保在过期前更新新鲜度 - 对长期会话,避免设置“要求新鲜会话”的门槛
- 接受这是设计如此: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
问题 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`
迁移指南
从 Clerk 迁移
主要差异:
- Clerk:第三方服务 → better-auth:可自托管
- Clerk:专有 → better-auth:开源
- Clerk:按月收费 → better-auth:免费
迁移步骤:
- 从 Clerk 导出用户数据(CSV 或 API)
- 导入 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, }); } - 将 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; - 更新中间件以验证会话
- 配置社交服务提供商(使用相同的 OAuth 应用,但配置不同)
从 Auth.js(NextAuth)迁移
主要差异:
- Auth.js:功能有限 → better-auth:功能全面(2FA、组织等)
- Auth.js:依赖大量回调 → better-auth:基于插件
- Auth.js:会话处理方式各异 → better-auth:处理方式一致
迁移步骤:
- 数据库架构:Auth.js 和 better-auth 使用相似的架构,但列名不同
- 替换配置:
// 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: { /* ... */ }, }, }); - 更新客户端钩子:
// Before import { useSession } from "next-auth/react"; // After import { authClient } from "@/lib/auth-client"; const { data: session } = authClient.useSession();
其他资源
官方文档
- 主页:https://better-auth.com
- 简介:https://www.better-auth.com/docs/introduction
- 安装:https://www.better-auth.com/docs/installation
- 基本用法:https://www.better-auth.com/docs/basic-usage
核心概念
- 会话管理:https://www.better-auth.com/docs/concepts/session-management
- 用户与账户:https://www.better-auth.com/docs/concepts/users-accounts
- 客户端 SDK:https://www.better-auth.com/docs/concepts/client
- 插件系统:https://www.better-auth.com/docs/concepts/plugins
身份验证方式
- 邮箱与密码:https://www.better-auth.com/docs/authentication/email-password
- OAuth 提供商:https://www.better-auth.com/docs/concepts/oauth
插件文档
核心插件:
- 双重身份验证(2FA):https://www.better-auth.com/docs/plugins/2fa
- 组织:https://www.better-auth.com/docs/plugins/organization
- 管理员:https://www.better-auth.com/docs/plugins/admin
- 多会话:https://www.better-auth.com/docs/plugins/multi-session
- API 密钥:https://www.better-auth.com/docs/plugins/api-key
- 通用 OAuth:https://www.better-auth.com/docs/plugins/generic-oauth
无密码插件:
- Passkey:https://www.better-auth.com/docs/plugins/passkey
- 魔法链接:https://www.better-auth.com/docs/plugins/magic-link
- 邮箱 OTP:https://www.better-auth.com/docs/plugins/email-otp
- 电话号码:https://www.better-auth.com/docs/plugins/phone-number
- 匿名用户:https://www.better-auth.com/docs/plugins/anonymous
高级插件:
- 用户名:https://www.better-auth.com/docs/plugins/username
- JWT:https://www.better-auth.com/docs/plugins/jwt
- OpenAPI:https://www.better-auth.com/docs/plugins/open-api
- OIDC 提供商:https://www.better-auth.com/docs/plugins/oidc-provider
- SSO:https://www.better-auth.com/docs/plugins/sso
- Stripe:https://www.better-auth.com/docs/plugins/stripe
- MCP:https://www.better-auth.com/docs/plugins/mcp
框架集成
- TanStack Start:https://www.better-auth.com/docs/integrations/tanstack
- Expo(React Native):https://www.better-auth.com/docs/integrations/expo
社区与支持
- GitHub:https://github.com/better-auth/better-auth(22.4k ⭐)
- 示例:https://github.com/better-auth/better-auth/tree/main/examples
- Discord:https://discord.gg/better-auth
- 更新日志:https://github.com/better-auth/better-auth/releases
相关文档
- Drizzle ORM:https://orm.drizzle.team/docs/get-started-sqlite
- Kysely:https://kysely.dev/
生产环境示例
已验证可运行的 D1 仓库(均使用 Drizzle 或 Kysely):
- zpg6/better-auth-cloudflare - Drizzle + D1(包含 CLI)
- zwily/example-react-router-cloudflare-d1-drizzle-better-auth - Drizzle + D1
- foxlau/react-router-v7-better-auth - Drizzle + D1
- matthewlynch/better-auth-react-router-cloudflare-d1 - Kysely + D1
没有任何一个使用直接的 d1Adapter - 都需要 Drizzle/Kysely。
版本兼容性
已测试版本:
[email protected][email protected][email protected][email protected][email protected]@cloudflare/workers-types@latest[email protected]- Node.js 18+,Bun 1.0+
破坏性变更:
- v1.4.6:
allowImpersonatingAdmins默认值为false - v1.4.0:仅支持 ESM(不支持 CommonJS)
- v1.3.0:多团队表结构变更
查看更新日志:https://github.com/better-auth/better-auth/releases
社区资源
Cloudflare 专属指南:
- zpg6/better-auth-cloudflare - Drizzle + D1 参考指南
- Hono + better-auth on Cloudflare - Hono 官方示例
- React Router + Cloudflare D1 - React Router v7 指南
- SvelteKit + Cloudflare D1 - SvelteKit 指南
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。