better-auth
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesebetter-auth - D1 Adapter & Error Prevention Guide
better-auth - D1适配器与错误预防指南
Package: better-auth@1.4.16 (Jan 21, 2026)
Breaking Changes: ESM-only (v1.4.0), Admin impersonation prevention default (v1.4.6), Multi-team table changes (v1.3), D1 requires Drizzle/Kysely (no direct adapter)
包版本:better-auth@1.4.16(2026年1月21日)
重大变更:仅支持ESM(v1.4.0)、默认禁用管理员 impersonation(v1.4.6)、多团队表结构变更(v1.3)、D1需搭配Drizzle/Kysely使用(无直接适配器)
⚠️ CRITICAL: D1 Adapter Requirement
⚠️ 关键提示:D1适配器要求
better-auth DOES NOT have . You MUST use:
d1Adapter()- Drizzle ORM (recommended):
drizzleAdapter(db, { provider: "sqlite" }) - Kysely:
new Kysely({ dialect: new D1Dialect({ database: env.DB }) })
See Issue #1 below for details.
better-auth 没有内置。你必须使用以下方式之一:
d1Adapter()- Drizzle ORM(推荐):
drizzleAdapter(db, { provider: "sqlite" }) - Kysely:
new Kysely({ dialect: new D1Dialect({ database: env.DB }) })
详情见下方问题1。
What's New in v1.4.10 (Dec 31, 2025)
v1.4.10版本新特性(2025年12月31日)
Major Features:
- OAuth 2.1 Provider plugin - Build your own OAuth provider (replaces MCP plugin)
- Patreon OAuth provider - Social sign-in with Patreon
- Kick OAuth provider - With refresh token support
- Vercel OAuth provider - Sign in with Vercel
- Global config - Deferred actions for better performance
backgroundTasks - Form data support - Email authentication with fetch metadata fallback
- Stripe enhancements - Flexible subscription lifecycle, option
disableRedirect
Admin Plugin Updates:
- ⚠️ Breaking: Impersonation of admins disabled by default (v1.4.6)
- Support role with permission-based user updates
- Role type inference improvements
Security Fixes:
- SAML XML parser hardening with configurable size constraints
- SAML assertion timestamp validation with per-provider clock skew
- SSO domain-verified provider trust
- Deprecated algorithm rejection
- Line nonce enforcement
主要功能:
- OAuth 2.1 Provider插件 - 构建自定义OAuth提供商(替代MCP插件)
- Patreon OAuth提供商 - 支持Patreon社交登录
- Kick OAuth提供商 - 支持刷新令牌
- Vercel OAuth提供商 - 支持Vercel登录
- 全局配置 - 延迟执行操作以提升性能
backgroundTasks - 表单数据支持 - 邮箱认证,回退使用fetch元数据
- Stripe增强 - 灵活的订阅生命周期管理、新增选项
disableRedirect
Admin插件更新:
- ⚠️ 重大变更:默认禁用管理员impersonation(v1.4.6)
- 支持基于权限的用户角色更新
- 角色类型推断优化
安全修复:
- 强化SAML XML解析器,支持配置大小限制
- 支持按提供商配置时钟偏移的SAML断言时间戳验证
- SSO域名验证的提供商信任机制
- 拒绝使用已弃用的算法
- 强制使用Line nonce
What's New in v1.4.0 (Nov 22, 2025)
v1.4.0版本新特性(2025年11月22日)
Major Features:
- Stateless session management - Sessions without database storage
- ESM-only package ⚠️ Breaking: CommonJS no longer supported
- JWT key rotation - Automatic key rotation for enhanced security
- SCIM provisioning - Enterprise user provisioning protocol
- @standard-schema/spec - Replaces ZodType for validation
- CaptchaFox integration - Built-in CAPTCHA support
- Automatic server-side IP detection
- Cookie-based account data storage
- Multiple passkey origins support
- RP-Initiated Logout endpoint (OIDC)
主要功能:
- 无状态会话管理 - 无需数据库存储会话
- 仅支持ESM包 ⚠️ 重大变更:不再支持CommonJS
- JWT密钥轮换 - 自动轮换密钥以增强安全性
- SCIM配置 - 企业用户配置协议
- @standard-schema/spec - 替代ZodType用于验证
- CaptchaFox集成 - 内置CAPTCHA支持
- 自动服务器端IP检测
- 基于Cookie的账户数据存储
- 支持多通行密钥源
- RP发起的登出端点(OIDC)
What's New in v1.3 (July 2025)
v1.3版本新特性(2025年7月)
Major Features:
- SSO with SAML 2.0 - Enterprise single sign-on (moved to separate package)
@better-auth/sso - Multi-team support ⚠️ Breaking: removed from member table, new
teamIdtable requiredteamMembers - Additional fields - Custom fields for organization/member/invitation models
- Performance improvements and bug fixes
主要功能:
- SAML 2.0单点登录 - 企业级单点登录(迁移至独立包)
@better-auth/sso - 多团队支持 ⚠️ 重大变更:从成员表移除,需新增
teamId表teamMembers - 额外字段 - 为组织/成员/邀请模型添加自定义字段
- 性能优化与Bug修复
Alternative: Kysely Adapter Pattern
替代方案:Kysely适配器模式
If you prefer Kysely over Drizzle:
File:
src/auth.tstypescript
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
});
}Why CamelCasePlugin?
If your Drizzle schema uses column names (e.g., ), but better-auth expects (e.g., ), the automatically converts between the two.
snake_caseemail_verifiedcamelCaseemailVerifiedCamelCasePlugin⚠️ Cloudflare Workers Note: D1 database bindings are only available inside the request handler (the function). You cannot initialize better-auth outside the request context. Use a factory function pattern:
fetch()typescript
// ❌ 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)
}
}Community Validation: Multiple production implementations confirm this pattern (Medium, AnswerOverflow, official Hono examples).
如果你偏好Kysely而非Drizzle:
文件:
src/auth.tstypescript
import { betterAuth } from "better-auth";
import { Kysely, CamelCasePlugin } from "kysely";
import { D1Dialect } from "kysely-d1";
type Env = {
DB: D1Database;
BETTER_AUTH_SECRET: string;
// ... 其他环境变量
};
export function createAuth(env: Env) {
return betterAuth({
secret: env.BETTER_AUTH_SECRET,
// 搭配D1Dialect的Kysely
database: {
db: new Kysely({
dialect: new D1Dialect({
database: env.DB,
}),
plugins: [
// 关键:如果Drizzle schema使用snake_case,则必须添加
new CamelCasePlugin(),
],
}),
type: "sqlite",
},
emailAndPassword: {
enabled: true,
},
// ... 其他配置
});
}为什么需要CamelCasePlugin?
如果你的Drizzle schema使用列名(例如),但better-auth期望使用(例如),会自动在两种格式间转换。
snake_caseemail_verifiedcamelCaseemailVerifiedCamelCasePlugin⚠️ Cloudflare Workers注意事项:D1数据库绑定仅在请求处理程序(函数)内部可用。你不能在请求上下文之外初始化better-auth,请使用工厂函数模式:
fetch()typescript
// ❌ 错误 - 上下文外无法获取DB绑定
const db = drizzle(env.DB, { schema }) // 此处env.DB不存在
export const auth = betterAuth({ database: drizzleAdapter(db, { provider: "sqlite" }) })
// ✅ 正确 - 为每个请求创建auth实例
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示例)。
Framework Integrations
框架集成
TanStack Start
TanStack Start
⚠️ CRITICAL: TanStack Start requires the plugin to handle cookie setting properly.
reactStartCookiestypescript
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
],
});Why it's needed: TanStack Start uses a special cookie handling system. Without this plugin, auth functions like and won't set cookies properly, causing authentication to fail.
signInEmail()signUpEmail()Important: The plugin must be the last plugin in the array.
reactStartCookiesSession Nullability Pattern: When using in TanStack Start, the session object always exists, but and are when not logged in:
useSession()session.usersession.sessionnulltypescript
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
}Always check or , not just . This is expected behavior (session object container always exists).
session?.usersession?.sessionsessionAPI Route Setup ():
/src/routes/api/auth/$.tstypescript
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),
},
},
})📚 Official Docs: https://www.better-auth.com/docs/integrations/tanstack
⚠️ 关键提示:TanStack Start需要插件才能正确处理Cookie设置。
reactStartCookiestypescript
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(), // ⚠️ 必须是最后一个插件
],
});为什么需要它:TanStack Start使用特殊的Cookie处理系统。如果没有此插件,和等认证函数将无法正确设置Cookie,导致认证失败。
signInEmail()signUpEmail()重要提示:插件必须是插件数组中的最后一个。
reactStartCookies会话空值模式:在TanStack Start中使用时,会话对象始终存在,但未登录时和为:
useSession()session.usersession.sessionnulltypescript
const { data: session } = authClient.useSession()
// 未登录时:
console.log(session) // { user: null, session: null }
console.log(!!session) // true(不符合预期!)
// 正确的检查方式:
if (session?.user) {
// 用户已登录
}请始终检查或,而不是仅检查。这是预期行为(会话对象容器始终存在)。
session?.usersession?.sessionsessionAPI路由配置():
/src/routes/api/auth/$.tstypescript
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),
},
},
})Available Plugins (v1.4+)
可用插件(v1.4+)
Better Auth provides plugins for advanced authentication features:
| Plugin | Import | Description | Docs |
|---|---|---|---|
| OAuth 2.1 Provider | | Build OAuth 2.1 provider with PKCE, JWT tokens, consent flows (replaces MCP & OIDC plugins) | 📚 |
| SSO | | Enterprise Single Sign-On with OIDC, OAuth2, and SAML 2.0 support | 📚 |
| Stripe | | Payment and subscription management with flexible lifecycle handling | 📚 |
| MCP | | ⚠️ Deprecated - Use OAuth 2.1 Provider instead | 📚 |
| Expo | | React Native/Expo with | 📚 |
Better Auth提供用于高级认证功能的插件:
| 插件 | 导入路径 | 描述 | 文档 |
|---|---|---|---|
| OAuth 2.1 Provider | | 构建支持PKCE、JWT令牌、授权流程的OAuth 2.1提供商(替代MCP & OIDC插件) | 📚 |
| SSO | | 企业级单点登录,支持OIDC、OAuth2和SAML 2.0 | 📚 |
| Stripe | | 支付与订阅管理,支持灵活的生命周期处理 | 📚 |
| MCP | | ⚠️ 已弃用 - 请使用OAuth 2.1 Provider替代 | 📚 |
| Expo | | React Native/Expo集成,支持 | 📚 |
OAuth 2.1 Provider Plugin (New in v1.4.9)
OAuth 2.1 Provider插件(v1.4.9新增)
Build your own OAuth provider for MCP servers, third-party apps, or API access:
typescript
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
}),
],
});Key Features:
- OAuth 2.1 compliant - PKCE mandatory, S256 only, no implicit flow
- Three grant types: ,
authorization_code,refresh_tokenclient_credentials - JWT or opaque tokens - Configurable token format
- Dynamic client registration - RFC 7591 compliant
- Consent management - Skip consent for trusted clients
- OIDC UserInfo endpoint - with scope-based claims
/oauth2/userinfo
Required Well-Known Endpoints:
typescript
// 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
});
}Create OAuth Client:
typescript
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) }⚠️ Note: This plugin is in active development and may not be suitable for production use yet.
为MCP服务器、第三方应用或API访问构建自定义OAuth提供商:
typescript
import { betterAuth } from "better-auth";
import { oauthProvider } from "better-auth/plugins";
import { jwt } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
jwt(), // 令牌签名必需
oauthProvider({
// 令牌过期时间(秒)
accessTokenExpiresIn: 3600, // 1小时
refreshTokenExpiresIn: 2592000, // 30天
authorizationCodeExpiresIn: 600, // 10分钟
}),
],
});核心功能:
- 兼容OAuth 2.1 - 强制使用PKCE、仅支持S256、无隐式流程
- 三种授权类型:、
authorization_code、refresh_tokenclient_credentials - JWT或不透明令牌 - 可配置令牌格式
- 动态客户端注册 - 符合RFC 7591标准
- 授权管理 - 可信客户端可跳过授权
- OIDC UserInfo端点 - ,支持基于范围的声明
/oauth2/userinfo
必需的标准端点:
typescript
// 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`,
// ... 其他元数据
});
}创建OAuth客户端:
typescript
const client = await auth.api.createOAuthClient({
body: {
name: "My MCP Server",
redirectURLs: ["https://claude.ai/callback"],
type: "public", // 或"confidential"
},
});
// 返回:{ clientId, clientSecret(如果是confidential类型) }⚠️ 注意:此插件仍在积极开发中,可能暂不适合生产环境使用。
Additional Plugins Reference
其他插件参考
| Plugin | Description | Docs |
|---|---|---|
| Bearer | API token auth (alternative to cookies for APIs) | 📚 |
| One Tap | Google One Tap frictionless sign-in | 📚 |
| SCIM | Enterprise user provisioning (SCIM 2.0) | 📚 |
| Anonymous | Guest user access without PII | 📚 |
| Username | Username-based sign-in (alternative to email) | 📚 |
| Generic OAuth | Custom OAuth providers with PKCE | 📚 |
| Multi-Session | Multiple accounts in same browser | 📚 |
| API Key | Token-based auth with rate limits | 📚 |
Bearer Token Plugin
Bearer令牌插件
For API-only authentication (mobile apps, CLI tools, third-party integrations):
typescript
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") },
},
});适用于纯API认证场景(移动应用、CLI工具、第三方集成):
typescript
import { bearer } from "better-auth/plugins";
import { bearerClient } from "better-auth/client/plugins";
// 服务器端
export const auth = betterAuth({
plugins: [bearer()],
});
// 客户端 - 登录后存储令牌
const { token } = await authClient.signIn.email({ email, password });
localStorage.setItem("auth_token", token);
// 客户端 - 配置fetch自动携带令牌
const authClient = createAuthClient({
plugins: [bearerClient()],
fetchOptions: {
auth: { type: "Bearer", token: () => localStorage.getItem("auth_token") },
},
});Google One Tap Plugin
Google一键登录插件
Frictionless single-tap sign-in for users already signed into Google:
typescript
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";
},
});Requirement: Configure authorized JavaScript origins in Google Cloud Console.
针对已登录Google的用户,实现无摩擦的一键登录:
typescript
import { oneTap } from "better-auth/plugins";
import { oneTapClient } from "better-auth/client/plugins";
// 服务器端
export const auth = betterAuth({
plugins: [oneTap()],
});
// 客户端
authClient.oneTap({
onSuccess: (session) => {
window.location.href = "/dashboard";
},
});要求:在Google Cloud Console中配置授权的JavaScript源。
Anonymous Plugin
Anonymous插件
Guest access without requiring email/password:
typescript
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无需邮箱/密码的访客访问:
typescript
import { anonymous } from "better-auth/plugins";
// 服务器端
export const auth = betterAuth({
plugins: [
anonymous({
emailDomainName: "anon.example.com", // 格式为temp@{id}.anon.example.com
onLinkAccount: async ({ anonymousUser, newUser }) => {
// 将匿名用户数据迁移至关联账户
await migrateUserData(anonymousUser.id, newUser.id);
},
}),
],
});
// 客户端
await authClient.signIn.anonymous();
// 后续:用户可通过social/email登录关联真实账户Generic OAuth Plugin
Generic OAuth插件
Add custom OAuth providers not in the built-in list:
typescript
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
},
],
}),
],
});Callback URL pattern:
{baseURL}/api/auth/oauth2/callback/{providerId}添加内置列表中没有的自定义OAuth提供商:
typescript
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, // 推荐启用
},
],
}),
],
});回调URL格式:
{baseURL}/api/auth/oauth2/callback/{providerId}Rate Limiting
速率限制
Built-in rate limiting with customizable rules:
typescript
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),
},
});Note: Server-side calls via bypass rate limiting.
auth.api.*内置可自定义规则的速率限制:
typescript
export const auth = betterAuth({
rateLimit: {
window: 60, // 时间窗口(秒,默认60)
max: 100, // 时间窗口内最大请求数(默认100)
// 敏感端点的自定义规则
customRules: {
"/sign-in/email": { window: 10, max: 3 },
"/two-factor/*": { window: 10, max: 3 },
"/forget-password": { window: 60, max: 5 },
},
// 分布式系统使用Redis/KV存储
storage: "secondary-storage", // 或"database"
},
// 速率限制的二级存储
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.*Stateless Sessions (v1.4.0+)
无状态会话(v1.4.0+)
Store sessions entirely in signed cookies without database storage:
typescript
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
},
});When to Use:
| Storage Type | Use Case | Tradeoffs |
|---|---|---|
| Stateless (cookie-only) | Read-heavy apps, edge/serverless, no revocation needed | Can't revoke sessions, limited payload size |
| D1 Database | Full session management, audit trails, revocation | Eventual consistency issues |
| KV Storage | Strong consistency, high read performance | Extra binding setup |
Key Points:
- Stateless sessions can't be revoked (user must wait for expiry)
- Cookie size limit ~4KB (limits session data)
- Use for interoperability,
encoding: "jwt"for encrypted"jwe" - Server must have consistent across all instances
BETTER_AUTH_SECRET
完全在签名Cookie中存储会话,无需数据库:
typescript
export const auth = betterAuth({
session: {
// 无状态:无需数据库存储,会话仅存在于Cookie中
storage: undefined, // 或直接省略
// Cookie配置
cookieCache: {
enabled: true,
maxAge: 60 * 60 * 24 * 7, // 7天
encoding: "jwt", // 无状态场景使用JWT(而非"compact")
},
// 会话过期时间
expiresIn: 60 * 60 * 24 * 7, // 7天
},
});适用场景:
| 存储类型 | 适用场景 | 权衡 |
|---|---|---|
| 无状态(仅Cookie) | 读密集型应用、边缘/无服务器环境、无需会话吊销 | 无法吊销会话、有效载荷大小受限 |
| D1数据库 | 完整会话管理、审计追踪、会话吊销 | 最终一致性问题 |
| KV存储 | 强一致性、高读取性能 | 需额外配置绑定 |
核心要点:
- 无状态会话无法吊销(用户必须等待过期)
- Cookie大小限制约为4KB(限制会话数据量)
- 使用实现互操作性,
encoding: "jwt"用于加密场景"jwe" - 服务器所有实例必须使用一致的
BETTER_AUTH_SECRET
JWT Key Rotation (v1.4.0+)
JWT密钥轮换(v1.4.0+)
Automatically rotate JWT signing keys for enhanced security:
typescript
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,
}),
],
});Key Points:
- Key rotation prevents compromised key from having indefinite validity
- Old keys are kept temporarily to validate existing tokens
- JWKS endpoint at for external services
/api/auth/jwks - Use RS256 for public key verification (microservices)
- HS256 (default) for single-service apps
自动轮换JWT签名密钥以增强安全性:
typescript
import { jwt } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
jwt({
// 密钥轮换(可选,企业级安全)
keyRotation: {
enabled: true,
rotationInterval: 60 * 60 * 24 * 30, // 每30天轮换一次
keepPreviousKeys: 3, // 保留3个旧密钥用于验证
},
// 自定义签名算法(默认HS256)
algorithm: "RS256", // 需要非对称密钥
// JWKS端点(自动生成于/api/auth/jwks)
exposeJWKS: true,
}),
],
});核心要点:
- 密钥轮换可防止泄露的密钥长期有效
- 临时保留旧密钥以验证现有令牌
- 外部服务可通过端点获取JWKS
/api/auth/jwks - 微服务场景使用RS256进行公钥验证
- 单服务应用使用HS256(默认)
Provider Scopes Reference
提供商范围参考
Common OAuth providers and the scopes needed for user data:
| Provider | Scope | Returns |
|---|---|---|
| User ID only | |
| Email address, email_verified | |
| Name, avatar (picture), locale | |
| GitHub | | Email address (may be private) |
| Name, avatar, profile URL, bio | |
| Microsoft | | User ID only |
| Email address | |
| Name, locale | |
| Full profile from Graph API | |
| Discord | | Username, avatar, discriminator |
| Email address | |
| Apple | | First/last name (first auth only) |
| Email or relay address | |
| Patreon | | User ID, name |
| Email address | |
| Vercel | (auto) | Email, name, avatar |
Configuration Example:
typescript
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"],
},
}常见OAuth提供商及获取用户数据所需的范围:
| 提供商 | 范围 | 返回数据 |
|---|---|---|
| 仅用户ID | |
| 邮箱地址、email_verified | |
| 姓名、头像(picture)、地区 | |
| GitHub | | 邮箱地址(可能为私有) |
| 姓名、头像、个人主页URL、简介 | |
| Microsoft | | 仅用户ID |
| 邮箱地址 | |
| 姓名、地区 | |
| Graph API返回的完整个人资料 | |
| Discord | | 用户名、头像、判别符 |
| 邮箱地址 | |
| Apple | | 名/姓(仅首次认证时返回) |
| 邮箱或中继地址 | |
| Patreon | | 用户ID、姓名 |
| 邮箱地址 | |
| Vercel | (自动) | 邮箱、姓名、头像 |
配置示例:
typescript
socialProviders: {
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
scope: ["openid", "email", "profile"], // 获取所有用户数据
},
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
scope: ["user:email", "read:user"], // 获取邮箱+完整个人资料
},
microsoft: {
clientId: env.MS_CLIENT_ID,
clientSecret: env.MS_CLIENT_SECRET,
scope: ["openid", "email", "profile", "User.Read"],
},
}Session Cookie Caching
会话Cookie缓存
Three encoding strategies for session cookies:
| Strategy | Format | Use Case |
|---|---|---|
| Compact (default) | Base64url + HMAC-SHA256 | Smallest, fastest |
| JWT | Standard JWT | Interoperable |
| JWE | A256CBC-HS512 encrypted | Most secure |
typescript
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
},
});Fresh sessions: Some sensitive operations require recently created sessions. Configure to control this window.
freshAge会话Cookie的三种编码策略:
| 策略 | 格式 | 适用场景 |
|---|---|---|
| Compact(默认) | Base64url + HMAC-SHA256 | 体积最小、速度最快 |
| JWT | 标准JWT | 互操作性强 |
| JWE | A256CBC-HS512加密 | 安全性最高 |
typescript
export const auth = betterAuth({
session: {
cookieCache: {
enabled: true,
maxAge: 300, // 5分钟
encoding: "compact", // 或"jwt"、"jwe"
},
freshAge: 60 * 60 * 24, // 1天 - 需要新鲜会话的操作
},
});新鲜会话:部分敏感操作要求使用最近创建的会话。配置控制此时间窗口。
freshAgeNew Social Providers (v1.4.9+)
新增社交提供商(v1.4.9+)
typescript
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,
},
}typescript
socialProviders: {
// Patreon - 创作者经济平台
patreon: {
clientId: env.PATREON_CLIENT_ID,
clientSecret: env.PATREON_CLIENT_SECRET,
scope: ["identity", "identity[email]"],
},
// Kick - 流媒体平台(支持刷新令牌)
kick: {
clientId: env.KICK_CLIENT_ID,
clientSecret: env.KICK_CLIENT_SECRET,
},
// Vercel - 开发者平台
vercel: {
clientId: env.VERCEL_CLIENT_ID,
clientSecret: env.VERCEL_CLIENT_SECRET,
},
}Cloudflare Workers Requirements
Cloudflare Workers要求
⚠️ CRITICAL: Cloudflare Workers require AsyncLocalStorage support:
toml
undefined⚠️ 关键提示:Cloudflare Workers需要支持AsyncLocalStorage:
toml
undefinedwrangler.toml
wrangler.toml
compatibility_flags = ["nodejs_compat"]
compatibility_flags = ["nodejs_compat"]
or for older Workers:
旧版Workers使用:
compatibility_flags = ["nodejs_als"]
compatibility_flags = ["nodejs_als"]
Without this flag, better-auth will fail with context-related errors.
---
如果没有此标志,better-auth会因上下文相关错误而失败。
---Database Hooks
数据库钩子
Execute custom logic during database operations:
typescript
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 });
},
},
},
},
});Available hooks: , for , , , tables.
createupdateusersessionaccountverification在数据库操作期间执行自定义逻辑:
typescript
export const auth = betterAuth({
databaseHooks: {
user: {
create: {
before: async (user, ctx) => {
// 创建前验证或修改数据
if (user.email?.endsWith("@blocked.com")) {
throw new APIError("BAD_REQUEST", { message: "该邮箱域名不被允许" });
}
return { data: { ...user, role: "member" } };
},
after: async (user, ctx) => {
// 发送欢迎邮件、创建关联记录等
await sendWelcomeEmail(user.email);
await createDefaultWorkspace(user.id);
},
},
},
session: {
create: {
after: async (session, ctx) => {
// 审计日志
await auditLog.create({ action: "session_created", userId: session.userId });
},
},
},
},
});可用钩子:、、、表的、操作。
usersessionaccountverificationcreateupdateExpo/React Native Integration
Expo/React Native集成
Complete mobile integration pattern:
typescript
// 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 deep link setup:
json
{
"expo": {
"scheme": "myapp"
}
}Server trustedOrigins (development):
typescript
trustedOrigins: ["exp://**", "myapp://"]完整的移动应用集成模式:
typescript
// 使用安全存储的客户端配置
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
await authClient.signIn.social({
provider: "google",
callbackURL: "myapp://auth/callback", // 深度链接
});
// 或使用ID令牌验证(无重定向)
await authClient.signIn.social({
provider: "google",
idToken: {
token: googleIdToken,
nonce: generatedNonce,
},
});
// 认证请求
const cookie = await authClient.getCookie();
await fetch("https://api.example.com/data", {
headers: { Cookie: cookie },
credentials: "omit",
});app.json深度链接配置:
json
{
"expo": {
"scheme": "myapp"
}
}服务器trustedOrigins(开发环境):
typescript
trustedOrigins: ["exp://**", "myapp://"]API Reference
API参考
Overview: What You Get For Free
概述:开箱即用的功能
When you call , better-auth automatically exposes 80+ production-ready REST endpoints at . Every endpoint is also available as a server-side method via for programmatic use.
auth.handler()/api/auth/*auth.api.*This dual-layer API system means:
- Clients (React, Vue, mobile apps) call HTTP endpoints directly
- Server-side code (middleware, background jobs) uses methods
auth.api.* - Zero boilerplate - no need to write auth endpoints manually
Time savings: Building this from scratch = ~220 hours. With better-auth = ~4-8 hours. 97% reduction.
调用后,better-auth会自动在路径下暴露80+个生产就绪的REST端点。每个端点也可通过作为服务器端方法使用,以实现编程调用。
auth.handler()/api/auth/*auth.api.*这种双层API系统意味着:
- 客户端(React、Vue、移动应用)直接调用HTTP端点
- 服务器端代码(中间件、后台任务)使用方法
auth.api.* - 零样板代码 - 无需手动编写认证端点
时间节省:从零构建这些功能约需220小时。使用better-auth仅需约4-8小时。减少97%的开发时间。
Auto-Generated HTTP Endpoints
自动生成的HTTP端点
All endpoints are automatically exposed at when using .
/api/auth/*auth.handler()使用时,所有端点会自动暴露在路径下。
auth.handler()/api/auth/*Core Authentication Endpoints
核心认证端点
| Endpoint | Method | Description |
|---|---|---|
| POST | Register with email/password |
| POST | Authenticate with email/password |
| POST | Logout user |
| POST | Update password (requires current password) |
| POST | Initiate password reset flow |
| POST | Complete password reset with token |
| POST | Send email verification link |
| GET | Verify email with token ( |
| GET | Retrieve current session |
| GET | Get all active user sessions |
| POST | End specific session |
| POST | End all sessions except current |
| POST | End all user sessions |
| POST | Modify user profile (name, image) |
| POST | Update email address |
| POST | Add password to OAuth-only account |
| POST | Remove user account |
| GET | Get linked authentication providers |
| POST | Connect OAuth provider to account |
| POST | Disconnect provider |
| 端点 | 方法 | 描述 |
|---|---|---|
| POST | 邮箱/密码注册 |
| POST | 邮箱/密码认证 |
| POST | 用户登出 |
| POST | 修改密码(需要当前密码) |
| POST | 启动密码重置流程 |
| POST | 使用令牌完成密码重置 |
| POST | 发送邮箱验证链接 |
| GET | 使用令牌验证邮箱( |
| GET | 获取当前会话 |
| GET | 获取用户所有活跃会话 |
| POST | 终止指定会话 |
| POST | 终止除当前外的所有会话 |
| POST | 终止用户所有会话 |
| POST | 修改用户资料(姓名、头像) |
| POST | 更新邮箱地址 |
| POST | 为仅使用OAuth的账户添加密码 |
| POST | 删除用户账户 |
| GET | 获取关联的认证提供商 |
| POST | 关联OAuth提供商账户 |
| POST | 解除提供商关联 |
Social OAuth Endpoints
社交OAuth端点
| Endpoint | Method | Description |
|---|---|---|
| POST | Initiate OAuth flow (provider specified in body) |
| GET | OAuth callback handler (e.g., |
| GET | Retrieve provider access token |
Example OAuth flow:
typescript
// 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| 端点 | 方法 | 描述 |
|---|---|---|
| POST | 启动OAuth流程(请求体中指定提供商) |
| GET | OAuth回调处理程序(例如 |
| GET | 获取提供商访问令牌 |
OAuth流程示例:
typescript
// 客户端发起
await authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
// better-auth处理重定向至Google
// Google重定向回/api/auth/callback/google
// better-auth自动创建会话Plugin Endpoints
插件端点
Two-Factor Authentication (2FA Plugin)
双因素认证(2FA插件)
typescript
import { twoFactor } from "better-auth/plugins";| Endpoint | Method | Description |
|---|---|---|
| POST | Activate 2FA for user |
| POST | Deactivate 2FA |
| GET | Get QR code URI for authenticator app |
| POST | Validate TOTP code from authenticator |
| POST | Send OTP via email |
| POST | Validate email OTP |
| POST | Create recovery codes |
| POST | Use backup code for login |
| GET | View current backup codes |
typescript
import { twoFactor } from "better-auth/plugins";| 端点 | 方法 | 描述 |
|---|---|---|
| POST | 为用户启用2FA |
| POST | 为用户禁用2FA |
| GET | 获取认证器应用的二维码URI |
| POST | 验证认证器生成的TOTP码 |
| POST | 通过邮箱发送OTP |
| POST | 验证邮箱OTP |
| POST | 创建恢复码 |
| POST | 使用恢复码登录 |
| GET | 查看当前恢复码 |
Organization Plugin (Multi-Tenant SaaS)
组织插件(多租户SaaS)
typescript
import { organization } from "better-auth/plugins";Organizations (10 endpoints):
| Endpoint | Method | Description |
|---|---|---|
| POST | Create organization |
| GET | List user's organizations |
| GET | Get complete org details |
| PUT | Modify organization |
| DELETE | Remove organization |
| GET | Verify slug availability |
| POST | Set active organization context |
Members (8 endpoints):
| Endpoint | Method | Description |
|---|---|---|
| GET | Get organization members |
| POST | Add member directly |
| DELETE | Remove member |
| PUT | Change member role |
| GET | Get current member info |
| POST | Leave organization |
Invitations (7 endpoints):
| Endpoint | Method | Description |
|---|---|---|
| POST | Send invitation email |
| POST | Accept invite |
| POST | Reject invite |
| POST | Cancel pending invite |
| GET | Get invitation details |
| GET | List org invitations |
| GET | List user's pending invites |
Teams (8 endpoints):
| Endpoint | Method | Description |
|---|---|---|
| POST | Create team within org |
| GET | List organization teams |
| PUT | Modify team |
| DELETE | Remove team |
| POST | Set active team context |
| GET | List team members |
| POST | Add member to team |
| DELETE | Remove team member |
Permissions & Roles (6 endpoints):
| Endpoint | Method | Description |
|---|---|---|
| POST | Check if user has permission |
| POST | Create custom role |
| DELETE | Delete custom role |
| GET | List all roles |
| GET | Get role details |
| PUT | Modify role permissions |
typescript
import { organization } from "better-auth/plugins";组织(10个端点):
| 端点 | 方法 | 描述 |
|---|---|---|
| POST | 创建组织 |
| GET | 列出用户所属组织 |
| GET | 获取完整组织详情 |
| PUT | 修改组织信息 |
| DELETE | 删除组织 |
| GET | 验证slug可用性 |
| POST | 设置活跃组织上下文 |
成员(8个端点):
| 端点 | 方法 | 描述 |
|---|---|---|
| GET | 获取组织成员 |
| POST | 直接添加成员 |
| DELETE | 移除成员 |
| PUT | 修改成员角色 |
| GET | 获取当前成员信息 |
| POST | 退出组织 |
邀请(7个端点):
| 端点 | 方法 | 描述 |
|---|---|---|
| POST | 发送邀请邮件 |
| POST | 接受邀请 |
| POST | 拒绝邀请 |
| POST | 取消待处理邀请 |
| GET | 获取邀请详情 |
| GET | 列出组织邀请 |
| GET | 列出用户待处理邀请 |
团队(8个端点):
| 端点 | 方法 | 描述 |
|---|---|---|
| POST | 在组织内创建团队 |
| GET | 列出组织团队 |
| PUT | 修改团队信息 |
| DELETE | 删除团队 |
| POST | 设置活跃团队上下文 |
| GET | 列出团队成员 |
| POST | 添加成员至团队 |
| DELETE | 从团队移除成员 |
权限与角色(6个端点):
| 端点 | 方法 | 描述 |
|---|---|---|
| POST | 检查用户是否拥有指定权限 |
| POST | 创建自定义角色 |
| DELETE | 删除自定义角色 |
| GET | 列出所有角色 |
| GET | 获取角色详情 |
| PUT | 修改角色权限 |
Admin Plugin
Admin插件
typescript
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",
})| Endpoint | Method | Description |
|---|---|---|
| POST | Create user as admin |
| GET | List all users (with filters/pagination) |
| POST | Assign user role |
| POST | Change user password |
| PUT | Modify user details |
| DELETE | Delete user account |
| POST | Ban user account (with optional expiry) |
| POST | Unban user |
| GET | Get user's active sessions |
| DELETE | End specific user session |
| DELETE | End all user sessions |
| POST | Start impersonating user |
| POST | End impersonation session |
⚠️ Breaking Change (v1.4.6): now defaults to . Set to explicitly if you need admin-on-admin impersonation.
allowImpersonatingAdminsfalsetrueCustom Roles with Permissions (v1.4.10):
typescript
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,
},
})typescript
import { admin } from "better-auth/plugins";
// v1.4.10配置选项
admin({
defaultRole: "user",
adminRoles: ["admin"],
adminUserIds: ["user_abc123"], // 始终为指定用户授予管理员权限
impersonationSessionDuration: 3600, // 1小时(秒)
allowImpersonatingAdmins: false, // ⚠️ v1.4.6默认值变更
defaultBanReason: "违反服务条款",
bannedUserMessage: "您的账户已被暂停",
})| 端点 | 方法 | 描述 |
|---|---|---|
| POST | 以管理员身份创建用户 |
| GET | 列出所有用户(支持过滤/分页) |
| POST | 为用户分配角色 |
| POST | 修改用户密码 |
| PUT | 修改用户详情 |
| DELETE | 删除用户账户 |
| POST | 封禁用户账户(可选设置过期时间) |
| POST | 解封用户 |
| GET | 获取用户活跃会话 |
| DELETE | 终止用户指定会话 |
| DELETE | 终止用户所有会话 |
| POST | 开始模拟用户 |
| POST | 结束模拟会话 |
⚠️ 重大变更(v1.4.6):默认值改为。如果需要管理员间模拟,请显式设置为。
allowImpersonatingAdminsfalsetrue带权限的自定义角色(v1.4.10):
typescript
import { createAccessControl } from "better-auth/plugins/access";
// 定义资源与权限
const ac = createAccessControl({
user: ["create", "read", "update", "delete", "ban", "impersonate"],
project: ["create", "read", "update", "delete", "share"],
} as const);
// 创建自定义角色
const supportRole = ac.newRole({
user: ["read", "ban"], // 可查看和封禁用户
project: ["read"], // 可查看项目
});
const managerRole = ac.newRole({
user: ["read", "update"],
project: ["create", "read", "update", "delete"],
});
// 在插件中使用
admin({
ac,
roles: {
support: supportRole,
manager: managerRole,
},
})Other Plugin Endpoints
其他插件端点
Passkey Plugin (5 endpoints) - Docs:
- ,
/passkey/add,/sign-in/passkey,/passkey/list,/passkey/delete/passkey/update
Magic Link Plugin (2 endpoints) - Docs:
- ,
/sign-in/magic-link/magic-link/verify
Username Plugin (2 endpoints) - Docs:
- ,
/sign-in/username/username/is-available
Phone Number Plugin (5 endpoints) - Docs:
- ,
/sign-in/phone-number,/phone-number/send-otp,/phone-number/verify,/phone-number/request-password-reset/phone-number/reset-password
Email OTP Plugin (6 endpoints) - Docs:
- ,
/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 Plugin (1 endpoint) - Docs:
/sign-in/anonymous
JWT Plugin (2 endpoints) - Docs:
- (get JWT),
/token(public key for verification)/jwks
OpenAPI Plugin (2 endpoints) - Docs:
- (interactive API docs with Scalar UI)
/reference - (get OpenAPI spec as JSON)
/generate-openapi-schema
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个端点) - 文档:
- (获取JWT),
/token(验证用公钥)/jwks
OpenAPI插件(2个端点) - 文档:
- (交互式API文档,使用Scalar UI)
/reference - (获取OpenAPI规范JSON)
/generate-openapi-schema
Server-Side API Methods (auth.api.*
)
auth.api.*服务器端API方法(auth.api.*
)
auth.api.*Every HTTP endpoint has a corresponding server-side method. Use these for:
- Server-side middleware (protecting routes)
- Background jobs (user cleanup, notifications)
- Admin operations (bulk user management)
- Custom auth flows (programmatic session creation)
每个HTTP端点都有对应的服务器端方法。适用于:
- 服务器端中间件(保护路由)
- 后台任务(用户清理、通知)
- 管理员操作(批量用户管理)
- 自定义认证流程(编程式创建会话)
Core API Methods
核心API方法
typescript
// 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: "newemail@example.com" },
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,
});typescript
// 认证
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 });
// 会话管理
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,
});
// 用户管理
await auth.api.updateUser({
body: { name: "New Name", image: "https://..." },
headers: request.headers,
});
await auth.api.changeEmail({
body: { newEmail: "newemail@example.com" },
headers: request.headers,
});
await auth.api.deleteUser({
body: { password: "current_password" },
headers: request.headers,
});
// 账户关联
await auth.api.linkSocialAccount({
body: { provider: "google" },
headers: request.headers,
});
await auth.api.unlinkAccount({
body: { providerId: "google", accountId: "google_123" },
headers: request.headers,
});Plugin API Methods
插件API方法
2FA Plugin:
typescript
// 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 Plugin:
typescript
// 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 Plugin:
typescript
// 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,
});2FA插件:
typescript
// 启用2FA
const { totpUri, backupCodes } = await auth.api.enableTwoFactor({
body: { issuer: "MyApp" },
headers: request.headers,
});
// 验证TOTP码
await auth.api.verifyTOTP({
body: { code: "123456", trustDevice: true },
headers: request.headers,
});
// 生成恢复码
const { backupCodes } = await auth.api.generateBackupCodes({
headers: request.headers,
});组织插件:
typescript
// 创建组织
const org = await auth.api.createOrganization({
body: { name: "Acme Corp", slug: "acme" },
headers: request.headers,
});
// 添加成员
await auth.api.addMember({
body: {
userId: "user_123",
role: "admin",
organizationId: org.id,
},
headers: request.headers,
});
// 检查权限
const hasPermission = await auth.api.hasPermission({
body: {
organizationId: org.id,
permission: "users:delete",
},
headers: request.headers,
});Admin插件:
typescript
// 分页列出用户
const users = await auth.api.listUsers({
query: {
search: "john",
limit: 10,
offset: 0,
sortBy: "createdAt",
sortOrder: "desc",
},
headers: request.headers,
});
// 封禁用户
await auth.api.banUser({
body: {
userId: "user_123",
reason: "违反服务条款",
expiresAt: new Date("2025-12-31"),
},
headers: request.headers,
});
// 模拟用户(用于管理员支持)
const impersonationSession = await auth.api.impersonateUser({
body: {
userId: "user_123",
expiresIn: 3600, // 1小时
},
headers: request.headers,
});When to Use Which
何时使用哪种方式
| Use Case | Use HTTP Endpoints | Use |
|---|---|---|
| Client-side auth | ✅ Yes | ❌ No |
| Server middleware | ❌ No | ✅ Yes |
| Background jobs | ❌ No | ✅ Yes |
| Admin dashboards | ✅ Yes (from client) | ✅ Yes (from server) |
| Custom auth flows | ❌ No | ✅ Yes |
| Mobile apps | ✅ Yes | ❌ No |
| API routes | ✅ Yes (proxy to handler) | ✅ Yes (direct calls) |
Example: Protected Route Middleware
typescript
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 });
});| 适用场景 | 使用HTTP端点 | 使用 |
|---|---|---|
| 客户端认证 | ✅ 是 | ❌ 否 |
| 服务器中间件 | ❌ 否 | ✅ 是 |
| 后台任务 | ❌ 否 | ✅ 是 |
| 管理员面板 | ✅ 是(客户端调用) | ✅ 是(服务器调用) |
| 自定义认证流程 | ❌ 否 | ✅ 是 |
| 移动应用 | ✅ 是 | ❌ 否 |
| API路由 | ✅ 是(代理至handler) | ✅ 是(直接调用) |
示例:受保护路由中间件
typescript
import { Hono } from "hono";
import { createAuth } from "./auth";
import { createDatabase } from "./db";
const app = new Hono<{ Bindings: Env }>();
// 使用服务器端API的中间件
app.use("/api/protected/*", async (c, next) => {
const db = createDatabase(c.env.DB);
const auth = createAuth(db, c.env);
// 使用服务器端方法
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
return c.json({ error: "未授权" }, 401);
}
// 附加至上下文
c.set("user", session.user);
c.set("session", session.session);
await next();
});
// 受保护路由
app.get("/api/protected/profile", async (c) => {
const user = c.get("user");
return c.json({ user });
});Discovering Available Endpoints
发现可用端点
Use the OpenAPI plugin to see all endpoints in your configuration:
typescript
import { betterAuth } from "better-auth";
import { openAPI } from "better-auth/plugins";
export const auth = betterAuth({
database: /* ... */,
plugins: [
openAPI(), // Adds /api/auth/reference endpoint
],
});Interactive documentation: Visit
http://localhost:8787/api/auth/referenceThis shows a Scalar UI with:
- ✅ All available endpoints grouped by feature
- ✅ Request/response schemas with types
- ✅ Try-it-out functionality (test endpoints in browser)
- ✅ Authentication requirements
- ✅ Code examples in multiple languages
Programmatic access:
typescript
const schema = await auth.api.generateOpenAPISchema();
console.log(JSON.stringify(schema, null, 2));
// Returns full OpenAPI 3.0 spec使用OpenAPI插件查看配置中的所有端点:
typescript
import { betterAuth } from "better-auth";
import { openAPI } from "better-auth/plugins";
export const auth = betterAuth({
database: /* ... */,
plugins: [
openAPI(), // 添加/api/auth/reference端点
],
});交互式文档:访问
http://localhost:8787/api/auth/reference此页面展示Scalar UI,包含:
- ✅ 按功能分组的所有可用端点
- ✅ 带类型的请求/响应 schema
- ✅ 在线测试功能(浏览器中测试端点)
- ✅ 认证要求
- ✅ 多语言代码示例
编程式访问:
typescript
const schema = await auth.api.generateOpenAPISchema();
console.log(JSON.stringify(schema, null, 2));
// 返回完整的OpenAPI 3.0规范Quantified Time Savings
量化时间节省
Building from scratch (manual implementation):
- Core auth endpoints (sign-up, sign-in, OAuth, sessions): 40 hours
- Email verification & password reset: 10 hours
- 2FA system (TOTP, backup codes, email OTP): 20 hours
- Organizations (teams, invitations, RBAC): 60 hours
- Admin panel (user management, impersonation): 30 hours
- Testing & debugging: 50 hours
- Security hardening: 20 hours
Total manual effort: ~220 hours (5.5 weeks full-time)
With better-auth:
- Initial setup: 2-4 hours
- Customization & styling: 2-4 hours
Total with better-auth: 4-8 hours
Savings: ~97% development time
从零构建(手动实现):
- 核心认证端点(注册、登录、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%的开发时间
Key Takeaway
核心要点
better-auth provides 80+ production-ready endpoints covering:
- ✅ Core authentication (20 endpoints)
- ✅ 2FA & passwordless (15 endpoints)
- ✅ Organizations & teams (35 endpoints)
- ✅ Admin & user management (15 endpoints)
- ✅ Social OAuth (auto-configured callbacks)
- ✅ OpenAPI documentation (interactive UI)
You write zero endpoint code. Just configure features and call .
auth.handler()better-auth提供80+个生产就绪的端点,涵盖:
- ✅ 核心认证(20个端点)
- ✅ 2FA与无密码认证(15个端点)
- ✅ 组织与团队(35个端点)
- ✅ 管理员与用户管理(15个端点)
- ✅ 社交OAuth(自动配置回调)
- ✅ OpenAPI文档(交互式UI)
你无需编写任何端点代码。只需配置功能并调用即可。
auth.handler()Known Issues & Solutions
已知问题与解决方案
Issue 1: "d1Adapter is not exported" Error
问题1:"d1Adapter is not exported"错误
Problem: Code shows but this doesn't exist.
import { d1Adapter } from 'better-auth/adapters/d1'Symptoms: TypeScript error or runtime error about missing export.
Solution: Use Drizzle or Kysely instead:
typescript
// ❌ 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"
}Source: Verified from 4 production repositories using better-auth + D1
问题:代码中使用,但该导出不存在。
import { d1Adapter } from 'better-auth/adapters/d1'症状:TypeScript错误或运行时错误提示缺少导出。
解决方案:使用Drizzle或Kysely替代:
typescript
// ❌ 错误 - 该导出不存在
import { d1Adapter } from 'better-auth/adapters/d1'
database: d1Adapter(env.DB)
// ✅ 正确 - 使用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" })
// ✅ 正确 - 使用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的生产仓库中验证
Issue 2: Schema Generation Fails
问题2:Schema生成失败
Problem: doesn't create D1-compatible schema.
npx better-auth migrateSymptoms: Migration SQL has wrong syntax or doesn't work with D1.
Solution: Use Drizzle Kit to generate migrations:
bash
undefined问题:无法创建兼容D1的schema。
npx better-auth migrate症状:迁移SQL语法错误或无法在D1中运行。
解决方案:使用Drizzle Kit生成迁移:
bash
// 从Drizzle schema生成迁移
npx drizzle-kit generate
// 应用至D1
wrangler d1 migrations apply my-app-db --remote原因:Drizzle Kit生成的SQLite兼容SQL可在D1中正常运行。
Generate migration from Drizzle schema
问题3:"CamelCase"与"snake_case"列不匹配
npx drizzle-kit generate
问题:数据库中列名为,但better-auth期望。
email_verifiedemailVerified症状:会话读取失败,用户数据字段缺失。
⚠️ 关键提示(v1.4.10+):使用Kysely的会破坏better-auth适配器中的连接解析。该插件会将这类连接键转换为,导致会话查询中用户数据为null。
CamelCasePlugin_joined_user_user_id_joinedUserUserIdDrizzle解决方案:从一开始就使用camelCase定义schema(如示例所示)。
搭配CamelCasePlugin的Kysely解决方案:为better-auth使用独立的Kysely实例,不添加CamelCasePlugin:
typescript
// better-auth使用的DB(无CamelCasePlugin)
const authDb = new Kysely({
dialect: new D1Dialect({ database: env.DB }),
})
// 应用查询使用的DB(带CamelCasePlugin)
const appDb = new Kysely({
dialect: new D1Dialect({ database: env.DB }),
plugins: [new CamelCasePlugin()],
})
export const auth = betterAuth({
database: { db: authDb, type: "sqlite" },
})Apply to D1
问题4:D1最终一致性
wrangler d1 migrations apply my-app-db --remote
**Why**: Drizzle Kit generates SQLite-compatible SQL that works with D1.
---问题:写入后立即读取会话返回旧数据。
症状:用户登录后,下一次请求中返回null。
getSession()解决方案:使用Cloudflare KV存储会话(强一致性):
typescript
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.tomltoml
[[kv_namespaces]]
binding = "SESSIONS_KV"
id = "your-kv-namespace-id"Issue 3: "CamelCase" vs "snake_case" Column Mismatch
问题5:SPA应用CORS错误
Problem: Database has but better-auth expects .
email_verifiedemailVerifiedSymptoms: Session reads fail, user data missing fields.
⚠️ CRITICAL (v1.4.10+): Using Kysely's breaks join parsing in better-auth adapter. The plugin converts join keys like to , causing user data to be null in session queries.
CamelCasePlugin_joined_user_user_id_joinedUserUserIdSolution for Drizzle: Define schema with camelCase from the start (as shown in examples).
Solution for Kysely with CamelCasePlugin: Use separate Kysely instance without CamelCasePlugin for better-auth:
typescript
// 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" },
})Source: GitHub Issue #7136
问题:认证API与前端不在同一源时出现CORS错误。
症状:浏览器控制台中出现错误。
Access-Control-Allow-Origin解决方案:在Worker中配置CORS头,并确保匹配:
trustedOriginstypescript
import { cors } from "hono/cors";
// 关键:两者必须与前端源完全匹配
app.use(
"/api/auth/*",
cors({
origin: "http://localhost:5173", // 前端URL(无末尾斜杠)
credentials: true, // 允许Cookie
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
})
);
// 同时在better-auth配置中
export const auth = betterAuth({
trustedOrigins: ["http://localhost:5173"], // 与CORS源一致
// ...
});常见错误:
- 源URL拼写错误(末尾斜杠、http/https不匹配、端口错误)
- CORS配置与中的源不匹配
trustedOrigins - CORS中间件在认证路由之后注册(必须在之前)
Issue 4: D1 Eventual Consistency
问题6:OAuth重定向URI不匹配
Problem: Session reads immediately after write return stale data.
Symptoms: User logs in but returns null on next request.
getSession()Solution: Use Cloudflare KV for session storage (strong consistency):
typescript
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);
},
},
},
});
}Add to :
wrangler.tomltoml
[[kv_namespaces]]
binding = "SESSIONS_KV"
id = "your-kv-namespace-id"问题:社交登录失败,提示"redirect_uri_mismatch"错误。
症状:Google/GitHub OAuth在用户授权后返回错误。
解决方案:确保OAuth提供商设置中的URI完全匹配:
提供商设置:https://yourdomain.com/api/auth/callback/google
better-auth URL: https://yourdomain.com/api/auth/callback/google
❌ 错误:http/https不匹配、末尾斜杠、子域不匹配
✅ 正确:完全逐字符匹配检查better-auth回调URL:
typescript
// 格式始终为:{baseURL}/api/auth/callback/{provider}
const callbackURL = `${env.BETTER_AUTH_URL}/api/auth/callback/google`;
console.log("在Google Console中配置此URL:", callbackURL);Issue 5: CORS Errors for SPA Applications
问题7:依赖缺失
Problem: CORS errors when auth API is on different origin than frontend.
Symptoms: errors in browser console.
Access-Control-Allow-OriginSolution: Configure CORS headers in Worker and ensure match:
trustedOriginstypescript
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
// ...
});Common Mistakes:
- Typo in origin URL (trailing slash, http vs https, wrong port)
- Mismatched origins between CORS config and
trustedOrigins - CORS middleware registered AFTER auth routes (must be before)
Source: GitHub Issue #7434
问题:TypeScript错误或运行时错误提示缺少包。
症状:或类似错误。
Cannot find module 'drizzle-orm'解决方案:安装所有必需的包:
Drizzle方案:
bash
npm install better-auth drizzle-orm drizzle-kit @cloudflare/workers-typesKysely方案:
bash
npm install better-auth kysely kysely-d1 @cloudflare/workers-typesIssue 6: OAuth Redirect URI Mismatch
问题8:邮箱验证邮件未发送
Problem: Social sign-in fails with "redirect_uri_mismatch" error.
Symptoms: Google/GitHub OAuth returns error after user consent.
Solution: Ensure exact match in OAuth provider settings:
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 matchCheck better-auth callback URL:
typescript
// 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);问题:邮箱验证链接始终未收到。
症状:用户注册后未收到邮件。
解决方案:实现处理程序:
sendVerificationEmailtypescript
export const auth = betterAuth({
database: /* ... */,
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
},
emailVerification: {
sendVerificationEmail: async ({ user, url }) => {
// 使用你的邮件服务(SendGrid、Resend等)
await sendEmail({
to: user.email,
subject: "验证你的邮箱",
html: `
<p>点击下方链接验证你的邮箱:</p>
<a href="${url}">验证邮箱</a>
`,
});
},
sendOnSignUp: true,
autoSignInAfterVerification: true,
expiresIn: 3600, // 1小时
},
});Cloudflare场景:使用Cloudflare Email Routing或外部服务(Resend、SendGrid)。
Issue 7: Missing Dependencies
问题9:会话过期过快
Problem: TypeScript errors or runtime errors about missing packages.
Symptoms: or similar.
Cannot find module 'drizzle-orm'Solution: Install all required packages:
For Drizzle approach:
bash
npm install better-auth drizzle-orm drizzle-kit @cloudflare/workers-typesFor Kysely approach:
bash
npm install better-auth kysely kysely-d1 @cloudflare/workers-types问题:会话意外过期或永不过期。
症状:用户意外登出或登出后会话仍存在。
解决方案:配置会话过期时间:
typescript
export const auth = betterAuth({
database: /* ... */,
session: {
expiresIn: 60 * 60 * 24 * 7, // 7天(秒)
updateAge: 60 * 60 * 24, // 每24小时更新一次会话
},
});Issue 8: Email Verification Not Sending
问题10:社交提供商用户数据缺失
Problem: Email verification links never arrive.
Symptoms: User signs up, but no email received.
Solution: Implement handler:
sendVerificationEmailtypescript
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
},
});For Cloudflare: Use Cloudflare Email Routing or external service (Resend, SendGrid).
问题:社交登录成功,但用户数据缺失(姓名、头像)。
症状:Google/GitHub登录后为null。
session.user.name解决方案:请求额外的范围:
typescript
socialProviders: {
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
scope: ["openid", "email", "profile"], // 包含'profile'以获取姓名/头像
},
github: {
clientId: env.GITHUB_CLIENT_ID,
clientSecret: env.GITHUB_CLIENT_SECRET,
scope: ["user:email", "read:user"], // 'read:user'以获取完整个人资料
},
}Issue 9: Session Expires Too Quickly
问题11:Drizzle Schema TypeScript错误
Problem: Session expires unexpectedly or never expires.
Symptoms: User logged out unexpectedly or session persists after logout.
Solution: Configure session expiration:
typescript
export const auth = betterAuth({
database: /* ... */,
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days (in seconds)
updateAge: 60 * 60 * 24, // Update session every 24 hours
},
});问题:TypeScript提示schema类型错误。
症状:
Type 'DrizzleD1Database' is not assignable to...解决方案:从数据库导出正确的类型:
typescript
// 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 });
}Issue 10: Social Provider Missing User Data
问题12:Wrangler开发模式无法工作
Problem: Social sign-in succeeds but missing user data (name, avatar).
Symptoms: is null after Google/GitHub sign-in.
session.user.nameSolution: Request additional scopes:
typescript
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
},
}问题:因数据库错误失败。
wrangler dev症状:本地开发中出现"数据库未找到"或迁移错误。
解决方案:先在本地应用迁移:
bash
// 应用迁移至本地D1
wrangler d1 migrations apply my-app-db --local
// 然后启动开发服务器
wrangler devIssue 11: TypeScript Errors with Drizzle Schema
问题13:用户数据更新后UI未同步(搭配TanStack Query)
Problem: TypeScript complains about schema types.
Symptoms:
Type 'DrizzleD1Database' is not assignable to...Solution: Export proper types from database:
typescript
// 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 });
}问题:更新用户数据(如头像、姓名)后,尽管调用了,中的数据仍未更新。
queryClient.invalidateQueries()useSession()症状:头像或用户资料数据在更新后显示旧值。TanStack Query缓存显示更新后的数据,但better-auth会话仍显示旧值。
根本原因:better-auth使用nanostores管理会话状态,而非TanStack Query。调用仅会使React Query缓存失效,不会影响better-auth nanostore。
queryClient.invalidateQueries()解决方案:更新用户数据后手动通知nanostore:
typescript
// 更新用户数据
const { data, error } = await authClient.updateUser({
image: newAvatarUrl,
name: newName
})
if (!error) {
// 手动使better-auth会话状态失效
authClient.$store.notify('$sessionSignal')
// 可选:如果其他数据使用React Query,也使其失效
queryClient.invalidateQueries({ queryKey: ['user-profile'] })
}适用场景:
- 同时使用better-auth + TanStack Query
- 更新用户资料字段(姓名、头像、邮箱)
- 任何在客户端修改会话用户数据的操作
替代方案:调用的方法,但更直接:
useSession()refetch()$store.notify()typescript
const { data: session, refetch } = authClient.useSession()
// 更新后
await refetch()注意:是未公开的内部API。此模式已在生产环境验证,但未来better-auth版本可能变更。
$store来源:社区发现的模式,已在生产环境使用
Issue 12: Wrangler Dev Mode Not Working
问题14:apiKey表Schema与D1不匹配
Problem: fails with database errors.
wrangler devSymptoms: "Database not found" or migration errors in local dev.
Solution: Apply migrations locally first:
bash
undefined问题:better-auth CLI()在使用D1时失败,提示"Failed to initialize database adapter"。
npx @better-auth/cli generate症状:CLI无法连接至D1以 introspect schema。通过CLI运行迁移失败。
根本原因:CLI需要直接的SQLite连接,但D1需要使用Cloudflare的绑定API。
解决方案:跳过CLI,使用文档中的apiKey schema手动创建迁移:
sql
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不支持- 如果schema偏离,使用全新迁移模式(删除并重建表)
ALTER TABLE DROP COLUMN - 在Drizzle适配器配置中,使用(小写)作为表名映射
apikey
D1全新迁移模式:
sql
// 按依赖逆序删除
DROP TABLE IF EXISTS api_key;
DROP TABLE IF EXISTS session;
// ... 其他表
// 使用干净的schema重建
CREATE TABLE api_key (...);来源:D1 + better-auth apiKey插件生产环境调试
Apply migrations to local D1
问题15:Admin插件需要数据库角色(双重认证)
wrangler d1 migrations apply my-app-db --local
问题:即使自定义中间件通过,Admin插件方法如仍失败,提示"You are not allowed to list users"。
listUsers症状:自定义中间件(检查ADMIN_EMAILS环境变量)通过,但返回403。
requireAdminauth.api.listUsers()根本原因:better-auth Admin插件有两层授权:
- 你的中间件 - 自定义检查(如ADMIN_EMAILS)
- better-auth内部 - 检查数据库中的
user.role === 'admin'
必须同时通过两层检查才能使用Admin插件方法。
解决方案:在数据库中将用户角色设置为'admin':
sql
// 修复现有用户
UPDATE user SET role = 'admin' WHERE email = 'admin@example.com';或在初始设置后使用Admin UI/API设置角色。
原因:Admin插件的、、等方法都会检查数据库中的,而非你的自定义中间件逻辑。
listUsersbanUserimpersonateUseruser.role来源:生产环境调试 - 误导性错误信息通过发现根本原因
wrangler tailThen run dev server
问题16:Organization/Team的updated_at必须为可空
wrangler dev
---问题:尽管API返回"slug already exists",但组织创建仍因SQL约束错误失败。
症状:
- 错误信息显示"An organization with this slug already exists"
- 数据库表实际为空
- 显示:
wrangler tailFailed query: insert into "organization" ... values (?, ?, ?, null, null, ?, null)
根本原因:better-auth在创建时为插入(仅在更新时设置)。如果你的schema设置了约束,插入会失败。
updated_atnullNOT NULL解决方案:在schema和迁移中使为可空:
updated_attypescript
// Drizzle schema - 正确写法
export const organization = sqliteTable('organization', {
// ...
updatedAt: integer('updated_at', { mode: 'timestamp' }), // 不要加.notNull()
});
export const team = sqliteTable('team', {
// ...
updatedAt: integer('updated_at', { mode: 'timestamp' }), // 不要加.notNull()
});sql
// 迁移 - 正确写法
CREATE TABLE organization (
// ...
updated_at INTEGER // 不要加NOT NULL
);适用范围:和表(可能包括其他插件表)
organizationteam来源:生产环境调试 - 揭示了"slug已存在"误导性信息背后的实际SQL错误
wrangler tailIssue 13: User Data Updates Not Reflecting in UI (with TanStack Query)
问题17:API响应双重嵌套(listMembers等)
Problem: After updating user data (e.g., avatar, name), changes don't appear in despite calling .
useSession()queryClient.invalidateQueries()Symptoms: Avatar image or user profile data appears stale after successful update. TanStack Query cache shows updated data, but better-auth session still shows old values.
Root Cause: better-auth uses nanostores for session state management, not TanStack Query. Calling only invalidates React Query cache, not the better-auth nanostore.
queryClient.invalidateQueries()Solution: Manually notify the nanostore after updating user data:
typescript
// 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'] })
}When to use:
- Using better-auth + TanStack Query together
- Updating user profile fields (name, image, email)
- Any operation that modifies session user data client-side
Alternative: Call from , but is more direct:
refetch()useSession()$store.notify()typescript
const { data: session, refetch } = authClient.useSession()
// After update
await refetch()Note: is an undocumented internal API. This pattern is production-validated but may change in future better-auth versions.
$storeSource: Community-discovered pattern, production use verified
问题:自定义API端点返回双重嵌套数据,如。
{ members: { members: [...], total: N } }症状:UI中计数显示"undefined",尽管数据存在但列表为空。
根本原因:better-auth方法如返回。使用包装会导致双重嵌套。
listMembers{ members: [...], total: N }c.json({ members: result })解决方案:从better-auth响应中提取数组:
typescript
// ❌ 错误 - 双重嵌套
const result = await auth.api.listMembers({ ... });
return c.json({ members: result });
// 返回:{ members: { members: [...], total: N } }
// ✅ 正确 - 提取数组
const result = await auth.api.listMembers({ ... });
const members = result?.members || [];
return c.json({ members });
// 返回:{ members: [...] }受影响方法(返回对象而非数组):
- →
listMembers{ members: [...], total: N } - →
listUsers{ users: [...], total: N, limit: N } - →
listOrganizations(检查结构){ organizations: [...] } - →
listInvitations{ invitations: [...] }
模式:在包装到API响应前,始终检查better-auth方法的返回类型。
来源:生产环境调试 - UI显示"undefined"计数,API检查揭示嵌套问题
Issue 14: apiKey Table Schema Mismatch with D1
问题18:Expo Client fromJSONSchema崩溃(v1.4.16)
Problem: better-auth CLI () fails with "Failed to initialize database adapter" when using D1.
npx @better-auth/cli generateSymptoms: CLI cannot connect to D1 to introspect schema. Running migrations through CLI doesn't work.
Root Cause: The CLI expects a direct SQLite connection, but D1 requires Cloudflare's binding API.
Solution: Skip the CLI and create migrations manually using the documented apiKey schema:
sql
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
);Key Points:
- The table has exactly 21 columns (as of better-auth v1.4+)
- Column names use (e.g.,
snake_case, notrate_limit_time_window)rateLimitTimeWindow - D1 doesn't support - if schema drifts, use fresh migration pattern (drop and recreate tables)
ALTER TABLE DROP COLUMN - In Drizzle adapter config, use (lowercase) as the table name mapping
apikey
Fresh Migration Pattern for D1:
sql
-- 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 (...);Source: Production debugging with D1 + better-auth apiKey plugin
问题:在v1.4.16中,从导入时崩溃,提示。
@better-auth/expo/clientexpoClientTypeError: Cannot read property 'fromJSONSchema' of undefined症状:React Native/Expo应用在导入expoClient时立即运行时崩溃。
根本原因:PR #6933(Expo基于Cookie的OAuth状态修复)引入的回归。f4a9f15后的3个提交之一破坏了构建。
解决方案:
- 临时方案:使用f4a9f15提交的持续构建版本(回归前版本)
- 永久方案:等待修复(截至2026-01-20,Issue #7491已开放)
typescript
// v1.4.16中崩溃
import { expoClient } from '@better-auth/expo/client'
// 临时解决方法:使用f4a9f15的构建版本
// 或等待下一版本修复Issue 15: Admin Plugin Requires DB Role (Dual-Auth)
问题19:additionalFields string[]返回JSON字符串
Problem: Admin plugin methods like fail with "You are not allowed to list users" even though your middleware passes.
listUsersSymptoms: Custom middleware (checking ADMIN_EMAILS env var) passes, but returns 403.
requireAdminauth.api.listUsers()Root Cause: better-auth admin plugin has two authorization layers:
- Your middleware - Custom check (e.g., ADMIN_EMAILS)
- better-auth internal - Checks in database
user.role === 'admin'
Both must pass for admin plugin methods to work.
Solution: Set user role to 'admin' in the database:
sql
-- Fix for existing users
UPDATE user SET role = 'admin' WHERE email = 'admin@example.com';Or use the admin UI/API to set roles after initial setup.
Why: The admin plugin's , , , etc. all check in the database, not your custom middleware logic.
listUsersbanUserimpersonateUseruser.roleSource: Production debugging - misleading error message led to root cause discovery via
wrangler tail问题:v1.4.12后,中的字段在通过Drizzle直接查询时返回JSON字符串()而非原生数组。
additionalFieldstype: 'string[]''["a","b"]'症状:是字符串而非数组。期望数组的代码崩溃。
user.notificationTokens根本原因:在Drizzle适配器中,字段使用存储,期望传入数组。但better-auth v1.4.4+向Drizzle传入字符串,导致双重序列化。通过Drizzle直接查询时,值为字符串,但使用**better-auth **时,转换器会正确返回数组。
string[]mode: 'json'internalAdapter解决方案:
- **使用better-auth **而非直接查询Drizzle(内置转换器)
internalAdapter - 修改Drizzle schema,将string[]字段改为
.jsonb() - 手动解析JSON字符串直至修复
typescript
// 配置
additionalFields: {
notificationTokens: {
type: 'string[]',
required: true,
input: true,
},
}
// 创建用户
notificationTokens: ['token1', 'token2']
// 数据库结果(通过Drizzle直接查询)
// '["token1","token2"]'(字符串,非数组)Issue 16: Organization/Team updated_at Must Be Nullable
问题20:additionalFields的"returned"属性阻止输入
Problem: Organization creation fails with SQL constraint error even though API returns "slug already exists".
Symptoms:
- Error message says "An organization with this slug already exists"
- Database table is actually empty
- shows:
wrangler tailFailed query: insert into "organization" ... values (?, ?, ?, null, null, ?, null)
Root Cause: better-auth inserts for on creation (only sets it on updates). If your schema has constraint, insert fails.
nullupdated_atNOT NULLSolution: Make nullable in both schema and migrations:
updated_attypescript
// 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()
});sql
-- Migration - CORRECT
CREATE TABLE organization (
-- ...
updated_at INTEGER -- No NOT NULL
);Applies to: and tables (possibly other plugin tables)
organizationteamSource: Production debugging - revealed actual SQL error behind misleading "slug exists" message
wrangler tail问题:在上设置会阻止字段通过API保存,即使设置了。
additionalFieldsreturned: falseinput: true症状:通过API端点创建/更新时,字段从未保存至数据库。
根本原因:属性会同时阻止读取和写入操作,而非仅读取。属性应独立控制写入权限。
returned: falseinput: true解决方案:
- 如果需要API写入权限,不要使用
returned: false - 改用服务器端方法()写入
auth.api.*
typescript
// Organization插件配置
additionalFields: {
secretField: {
type: 'string',
required: true,
input: true, // 应允许API写入
returned: false, // 应仅阻止读取,但实际阻止了写入
},
}
// 创建组织的API请求
// secretField从未保存至数据库Issue 17: API Response Double-Nesting (listMembers, etc.)
问题21:freshAge基于创建时间而非活动时间
Problem: Custom API endpoints return double-nested data like .
{ members: { members: [...], total: N } }Symptoms: UI shows "undefined" for counts, empty lists despite data existing.
Root Cause: better-auth methods like return . Wrapping with creates double nesting.
listMembers{ members: [...], total: N }c.json({ members: result })Solution: Extract the array from better-auth response:
typescript
// ❌ 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: [...] }Affected methods (return objects, not arrays):
- →
listMembers{ members: [...], total: N } - →
listUsers{ users: [...], total: N, limit: N } - →
listOrganizations(check structure){ organizations: [...] } - →
listInvitations{ invitations: [...] }
Pattern: Always check better-auth method return types before wrapping in your API response.
Source: Production debugging - UI showed "undefined" count, API inspection revealed nesting issue
问题:检查的是会话创建后的时间,而非最近活动时间。即使持续使用,活跃会话在过后也会变为"不新鲜"。
session.freshAgefreshAge症状:需要"新鲜会话"的端点拒绝有效的活跃会话。
原因:检查,但仅在基于刷新会话时才会更新。如果,会话会在更新前变为"不新鲜"。
freshSessionMiddlewareDate.now() - (session.updatedAt || session.createdAt)updatedAtupdateAgeupdateAge > freshAgeupdatedAt解决方案:
- 设置,确保在过期前更新会话新鲜度
updateAge <= freshAge - 避免对长会话使用"新鲜会话"要求
- 接受设计:freshAge严格基于创建时间(维护者已确认)
typescript
// 配置
session: {
expiresIn: 60 * 60 * 24 * 7, // 7天
freshAge: 60 * 60 * 24, // 24小时
updateAge: 60 * 60 * 24 * 3, // 3天(> freshAge!) ⚠️ 问题
// 正确配置 - updateAge <= freshAge
updateAge: 60 * 60 * 12, // 12小时(< freshAge)
}
// 错误配置时间线:
// T+0h: 用户登录(createdAt = now)
// T+12h: 用户发起请求(会话活跃,仍新鲜)
// T+25h: 用户发起请求(会话活跃,但已不新鲜 - freshAge已过)
// 结果:需要新鲜会话的端点拒绝活跃会话Issue 18: Expo Client fromJSONSchema Crash (v1.4.16)
问题22:OAuth令牌端点返回嵌套JSON
Problem: Importing from crashes with on v1.4.16.
expoClient@better-auth/expo/clientTypeError: Cannot read property 'fromJSONSchema' of undefinedSymptoms: Runtime crash immediately when importing expoClient in React Native/Expo apps.
Root Cause: Regression introduced after PR #6933 (cookie-based OAuth state fix for Expo). One of 3 commits after f4a9f15 broke the build.
Solution:
- Temporary: Use continuous build at commit (pre-regression)
f4a9f15 - Permanent: Wait for fix (issue #7491 open as of 2026-01-20)
typescript
// Crashes on v1.4.16
import { expoClient } from '@better-auth/expo/client'
// Workaround: Use continuous build at f4a9f15
// Or wait for fix in next releaseSource: GitHub Issue #7491
问题:OAuth 2.1和OIDC令牌端点返回而非符合规范的顶层JSON。OAuth客户端期望在顶层。
{ "response": { ...tokens... } }{ "access_token": "...", "token_type": "bearer" }症状:OAuth客户端失败,提示或。
Bearer undefinedinvalid_token根本原因:端点流水线返回供内部使用,直接序列化为HTTP响应。这违反了OAuth/OIDC规范要求。
{ response, headers, status }解决方案:
- 临时方案:在客户端手动解析字段
.response - 永久方案:等待修复(Issue #7355已开放,接受贡献)
typescript
// 期望(符合规范)
{ "access_token": "...", "token_type": "bearer", "expires_in": 3600 }
// 实际(嵌套)
{ "response": { "access_token": "...", "token_type": "bearer", "expires_in": 3600 } }
// 结果:OAuth客户端解析失败,发送`Bearer undefined`Issue 19: additionalFields string[] Returns Stringified JSON
迁移指南
—
从Clerk迁移
Problem: After v1.4.12, with return stringified arrays () instead of native arrays when querying via Drizzle directly.
additionalFieldstype: 'string[]''["a","b"]'Symptoms: is a string, not an array. Code expecting arrays breaks.
user.notificationTokensRoot Cause: In Drizzle adapter, fields are stored with , which expects arrays. But better-auth v1.4.4+ passes strings to Drizzle, causing double-stringification. When querying directly via Drizzle, the value is a string, but when using better-auth , a transformer correctly returns an array.
string[]mode: 'json'internalAdapterSolution:
- Use better-auth instead of querying Drizzle directly (has transformer)
internalAdapter - Change Drizzle schema to for string[] fields
.jsonb() - Manually parse JSON strings until fixed
typescript
// 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)Source: GitHub Issue #7440
核心差异:
- Clerk:第三方服务 → better-auth:自托管
- Clerk:专有 → better-auth:开源
- Clerk:按月付费 → better-auth:免费
迁移步骤:
- 导出用户数据:从Clerk导出(CSV或API)
- 导入至better-auth数据库:
typescript
// 迁移脚本 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客户端:
typescript
// 之前(Clerk) import { useUser } from "@clerk/nextjs"; const { user } = useUser(); // 之后(better-auth) import { authClient } from "@/lib/auth-client"; const { data: session } = authClient.useSession(); const user = session?.user; - 更新中间件用于会话验证
- 配置社交提供商(使用相同的OAuth应用,不同的配置)
Issue 20: additionalFields "returned" Property Blocks Input
从Auth.js(NextAuth)迁移
Problem: Setting on prevents field from being saved via API, even with .
returned: falseadditionalFieldsinput: trueSymptoms: Field never saved to database when creating/updating via API endpoints.
Root Cause: The property blocks both read AND write operations, not just reads as intended. The property should control write access independently.
returned: falseinput: trueSolution:
- Don't use if you need API write access
returned: false - Write via server-side methods () instead
auth.api.*
typescript
// 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 databaseSource: GitHub Issue #7489
核心差异:
- Auth.js:功能有限 → better-auth:功能全面(2FA、组织等)
- Auth.js:回调密集 → better-auth:插件化
- Auth.js:会话处理不一致 → better-auth:一致
迁移步骤:
- 数据库schema:Auth.js和better-auth schema相似,但列名不同
- 替换配置:
typescript
// 之前(Auth.js) import NextAuth from "next-auth"; import GoogleProvider from "next-auth/providers/google"; export default NextAuth({ providers: [GoogleProvider({ /* ... */ })], }); // 之后(better-auth) import { betterAuth } from "better-auth"; export const auth = betterAuth({ socialProviders: { google: { /* ... */ }, }, }); - 更新客户端钩子:
typescript
// 之前 import { useSession } from "next-auth/react"; // 之后 import { authClient } from "@/lib/auth-client"; const { data: session } = authClient.useSession();
Issue 21: freshAge Based on Creation Time, Not Activity
其他资源
—
官方文档
Problem: checks time-since-creation, NOT recent activity. Active sessions become "not fresh" after elapses, even if used constantly.
session.freshAgefreshAgeSymptoms: "Fresh session required" endpoints reject valid active sessions.
Why It Happens: The checks , but only changes when the session is refreshed based on . If , the session becomes "not fresh" before is bumped.
freshSessionMiddlewareDate.now() - (session.updatedAt || session.createdAt)updatedAtupdateAgeupdateAge > freshAgeupdatedAtSolution:
- Set to ensure freshness is updated before expiry
updateAge <= freshAge - Avoid "fresh session required" gating for long-lived sessions
- Accept as design: freshAge is strictly time-since-creation (maintainer confirmed)
typescript
// 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 sessionSource: GitHub Issue #7472
Issue 22: OAuth Token Endpoints Return Wrapped JSON
核心概念
Problem: OAuth 2.1 and OIDC token endpoints return instead of spec-compliant top-level JSON. OAuth clients expect at root.
{ "response": { ...tokens... } }{ "access_token": "...", "token_type": "bearer" }Symptoms: OAuth clients fail with or .
Bearer undefinedinvalid_tokenRoot Cause: The endpoint pipeline returns for internal use, which gets serialized directly for HTTP requests. This breaks OAuth/OIDC spec requirements.
{ response, headers, status }Solution:
- Temporary: Manually unwrap field on client
.response - Permanent: Wait for fix (issue #7355 open, accepting contributions)
typescript
// 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`Source: GitHub Issue #7355
Migration Guides
认证方式
From Clerk
—
Key differences:
- Clerk: Third-party service → better-auth: Self-hosted
- Clerk: Proprietary → better-auth: Open source
- Clerk: Monthly cost → better-auth: Free
Migration steps:
- Export user data from Clerk (CSV or API)
- Import into better-auth database:
typescript
// 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, }); } - Replace Clerk SDK with better-auth client:
typescript
// 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; - Update middleware for session verification
- Configure social providers (same OAuth apps, different config)
From Auth.js (NextAuth)
插件文档
Key differences:
- Auth.js: Limited features → better-auth: Comprehensive (2FA, orgs, etc.)
- Auth.js: Callbacks-heavy → better-auth: Plugin-based
- Auth.js: Session handling varies → better-auth: Consistent
Migration steps:
- Database schema: Auth.js and better-auth use similar schemas, but column names differ
- Replace configuration:
typescript
// 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: { /* ... */ }, }, }); - Update client hooks:
typescript
// Before import { useSession } from "next-auth/react"; // After import { authClient } from "@/lib/auth-client"; const { data: session } = authClient.useSession();
核心插件:
- 2FA(双因素认证):https://www.better-auth.com/docs/plugins/2fa
- 组织:https://www.better-auth.com/docs/plugins/organization
- Admin:https://www.better-auth.com/docs/plugins/admin
- 多会话:https://www.better-auth.com/docs/plugins/multi-session
- API Key: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
- Magic Link: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
Additional Resources
框架集成
Official Documentation
—
- Homepage: https://better-auth.com
- Introduction: https://www.better-auth.com/docs/introduction
- Installation: https://www.better-auth.com/docs/installation
- Basic Usage: https://www.better-auth.com/docs/basic-usage
- TanStack Start:https://www.better-auth.com/docs/integrations/tanstack
- Expo(React Native):https://www.better-auth.com/docs/integrations/expo
Core Concepts
社区与支持
- Session Management: https://www.better-auth.com/docs/concepts/session-management
- Users & Accounts: https://www.better-auth.com/docs/concepts/users-accounts
- Client SDK: https://www.better-auth.com/docs/concepts/client
- Plugins System: https://www.better-auth.com/docs/concepts/plugins
Authentication Methods
相关文档
- Email & Password: https://www.better-auth.com/docs/authentication/email-password
- OAuth Providers: https://www.better-auth.com/docs/concepts/oauth
- Drizzle ORM:https://orm.drizzle.team/docs/get-started-sqlite
- Kysely:https://kysely.dev/
Plugin Documentation
生产示例
Core Plugins:
- 2FA (Two-Factor): https://www.better-auth.com/docs/plugins/2fa
- Organization: https://www.better-auth.com/docs/plugins/organization
- Admin: https://www.better-auth.com/docs/plugins/admin
- Multi-Session: https://www.better-auth.com/docs/plugins/multi-session
- API Key: https://www.better-auth.com/docs/plugins/api-key
- Generic OAuth: https://www.better-auth.com/docs/plugins/generic-oauth
Passwordless Plugins:
- Passkey: https://www.better-auth.com/docs/plugins/passkey
- Magic Link: https://www.better-auth.com/docs/plugins/magic-link
- Email OTP: https://www.better-auth.com/docs/plugins/email-otp
- Phone Number: https://www.better-auth.com/docs/plugins/phone-number
- Anonymous: https://www.better-auth.com/docs/plugins/anonymous
Advanced Plugins:
- Username: 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 Provider: 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
已验证的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
没有仓库使用直接的 - 均需搭配Drizzle/Kysely。
d1AdapterFramework Integrations
版本兼容性
- TanStack Start: https://www.better-auth.com/docs/integrations/tanstack
- Expo (React Native): https://www.better-auth.com/docs/integrations/expo
已测试版本:
better-auth@1.4.10drizzle-orm@0.45.1drizzle-kit@0.31.8kysely@0.28.9kysely-d1@0.4.0@cloudflare/workers-types@latesthono@4.11.3- Node.js 18+, Bun 1.0+
重大变更:
- v1.4.6:默认值改为
allowImpersonatingAdminsfalse - v1.4.0:仅支持ESM(无CommonJS)
- v1.3.0:多团队表结构变更
Community & Support
社区资源
- GitHub: https://github.com/better-auth/better-auth (22.4k ⭐)
- Examples: https://github.com/better-auth/better-auth/tree/main/examples
- Discord: https://discord.gg/better-auth
- Changelog: 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 tokens(D1适配器错误、15+插件、速率限制、会话缓存、数据库钩子、移动集成)
- 有此技能:~8,000 tokens(聚焦错误+模式+所有插件+API参考)
- 节省:~77%(~27,000 tokens)
避免的错误:22个带精确解决方案的已记录问题
核心价值:D1适配器要求、nodejs_compat标志、OAuth 2.1 Provider、Bearer/OneTap/SCIM/Anonymous插件、速率限制、会话缓存、数据库钩子、Expo集成、80+端点参考、additionalFields Bug、freshAge行为、OAuth令牌嵌套
最后验证:2026-01-21 | 技能版本:5.1.0 | 变更:添加了5个训练截止后研究的新问题(Expo fromJSONSchema崩溃、additionalFields string[] Bug、additionalFields returned属性Bug、freshAge非基于活动时间、OAuth令牌嵌套)。扩展了问题3中Kysely CamelCasePlugin连接解析失败的内容。扩展了问题5中Hono CORS模式。添加了Cloudflare Workers DB绑定约束说明。添加了TanStack Start会话空值模式。更新至v1.4.16。
Related Documentation
—
- Drizzle ORM: https://orm.drizzle.team/docs/get-started-sqlite
- Kysely: https://kysely.dev/
—
Production Examples
—
Verified working D1 repositories (all use Drizzle or Kysely):
- zpg6/better-auth-cloudflare - Drizzle + D1 (includes 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
None use a direct - all require Drizzle/Kysely.
d1Adapter—
Version Compatibility
—
Tested with:
better-auth@1.4.10drizzle-orm@0.45.1drizzle-kit@0.31.8kysely@0.28.9kysely-d1@0.4.0@cloudflare/workers-types@latesthono@4.11.3- Node.js 18+, Bun 1.0+
Breaking changes:
- v1.4.6: defaults to
allowImpersonatingAdminsfalse - v1.4.0: ESM-only (no CommonJS)
- v1.3.0: Multi-team table structure change
Check changelog: https://github.com/better-auth/better-auth/releases
—
Community Resources
—
Cloudflare-specific guides:
- zpg6/better-auth-cloudflare - Drizzle + D1 reference
- Hono + better-auth on Cloudflare - Official Hono example
- React Router + Cloudflare D1 - React Router v7 guide
- SvelteKit + Cloudflare D1 - SvelteKit guide
Token Efficiency:
- Without skill: ~35,000 tokens (D1 adapter errors, 15+ plugins, rate limiting, session caching, database hooks, mobile integration)
- With skill: ~8,000 tokens (focused on errors + patterns + all plugins + API reference)
- Savings: ~77% (~27,000 tokens)
Errors prevented: 22 documented issues with exact solutions
Key value: D1 adapter requirement, nodejs_compat flag, OAuth 2.1 Provider, Bearer/OneTap/SCIM/Anonymous plugins, rate limiting, session caching, database hooks, Expo integration, 80+ endpoint reference, additionalFields bugs, freshAge behavior, OAuth token wrapping
Last verified: 2026-01-21 | Skill version: 5.1.0 | Changes: Added 5 new issues from post-training-cutoff research (Expo fromJSONSchema crash, additionalFields string[] bug, additionalFields returned property bug, freshAge not activity-based, OAuth token wrapping). Expanded Issue #3 with Kysely CamelCasePlugin join parsing failure. Expanded Issue #5 with Hono CORS pattern. Added Cloudflare Workers DB binding constraints note. Added TanStack Start session nullability pattern. Updated to v1.4.16.
—