better-auth

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

better-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
d1Adapter()
. You MUST use:
  • 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
    backgroundTasks
    config
    - Deferred actions for better performance
  • Form data support - Email authentication with fetch metadata fallback
  • Stripe enhancements - Flexible subscription lifecycle,
    disableRedirect
    option
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
    @better-auth/sso
    package)
  • Multi-team support ⚠️ Breaking:
    teamId
    removed from member table, new
    teamMembers
    table required
  • 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.ts
typescript
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
snake_case
column names (e.g.,
email_verified
), but better-auth expects
camelCase
(e.g.,
emailVerified
), the
CamelCasePlugin
automatically converts between the two.
⚠️ Cloudflare Workers Note: D1 database bindings are only available inside the request handler (the
fetch()
function). You cannot initialize better-auth outside the request context. Use a factory function pattern:
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.ts
typescript
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使用
snake_case
列名(例如
email_verified
),但better-auth期望使用
camelCase
(例如
emailVerified
),
CamelCasePlugin
会自动在两种格式间转换。
⚠️ Cloudflare Workers注意事项:D1数据库绑定仅在请求处理程序(
fetch()
函数)内部可用。你不能在请求上下文之外初始化better-auth,请使用工厂函数模式:
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
reactStartCookies
plugin to handle cookie setting properly.
typescript
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
signInEmail()
and
signUpEmail()
won't set cookies properly, causing authentication to fail.
Important: The
reactStartCookies
plugin must be the last plugin in the array.
Session Nullability Pattern: When using
useSession()
in TanStack Start, the session object always exists, but
session.user
and
session.session
are
null
when not logged in:
typescript
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
session?.user
or
session?.session
, not just
session
. This is expected behavior (session object container always exists).
API Route Setup (
/src/routes/api/auth/$.ts
):
typescript
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),
    },
  },
})

⚠️ 关键提示:TanStack Start需要
reactStartCookies
插件才能正确处理Cookie设置。
typescript
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处理系统。如果没有此插件,
signInEmail()
signUpEmail()
等认证函数将无法正确设置Cookie,导致认证失败。
重要提示
reactStartCookies
插件必须是插件数组中的最后一个
会话空值模式:在TanStack Start中使用
useSession()
时,会话对象始终存在,但未登录时
session.user
session.session
null
typescript
const { data: session } = authClient.useSession()

// 未登录时:
console.log(session) // { user: null, session: null }
console.log(!!session) // true(不符合预期!)

// 正确的检查方式:
if (session?.user) {
  // 用户已登录
}
请始终检查
session?.user
session?.session
,而不是仅检查
session
。这是预期行为(会话对象容器始终存在)。
API路由配置
/src/routes/api/auth/$.ts
):
typescript
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:
PluginImportDescriptionDocs
OAuth 2.1 Provider
better-auth/plugins
Build OAuth 2.1 provider with PKCE, JWT tokens, consent flows (replaces MCP & OIDC plugins)📚
SSO
better-auth/plugins
Enterprise Single Sign-On with OIDC, OAuth2, and SAML 2.0 support📚
Stripe
better-auth/plugins
Payment and subscription management with flexible lifecycle handling📚
MCP
better-auth/plugins
⚠️ Deprecated - Use OAuth 2.1 Provider instead📚
Expo
better-auth/expo
React Native/Expo with
webBrowserOptions
and last-login-method tracking
📚
Better Auth提供用于高级认证功能的插件:
插件导入路径描述文档
OAuth 2.1 Provider
better-auth/plugins
构建支持PKCE、JWT令牌、授权流程的OAuth 2.1提供商(替代MCP & OIDC插件)📚
SSO
better-auth/plugins
企业级单点登录,支持OIDC、OAuth2和SAML 2.0📚
Stripe
better-auth/plugins
支付与订阅管理,支持灵活的生命周期处理📚
MCP
better-auth/plugins
⚠️ 已弃用 - 请使用OAuth 2.1 Provider替代📚
Expo
better-auth/expo
React Native/Expo集成,支持
webBrowserOptions
和最后登录方式跟踪
📚

OAuth 2.1 Provider 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_token
    ,
    client_credentials
  • JWT or opaque tokens - Configurable token format
  • Dynamic client registration - RFC 7591 compliant
  • Consent management - Skip consent for trusted clients
  • OIDC UserInfo endpoint -
    /oauth2/userinfo
    with scope-based claims
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_token
    client_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

其他插件参考

PluginDescriptionDocs
BearerAPI token auth (alternative to cookies for APIs)📚
One TapGoogle One Tap frictionless sign-in📚
SCIMEnterprise user provisioning (SCIM 2.0)📚
AnonymousGuest user access without PII📚
UsernameUsername-based sign-in (alternative to email)📚
Generic OAuthCustom OAuth providers with PKCE📚
Multi-SessionMultiple accounts in same browser📚
API KeyToken-based auth with rate limits📚
插件描述文档
BearerAPI令牌认证(API场景下替代Cookie)📚
One TapGoogle一键登录,无需额外操作📚
SCIM企业用户配置(SCIM 2.0)📚
Anonymous无需个人身份信息的访客用户访问📚
Username基于用户名的登录(替代邮箱)📚
Generic OAuth支持PKCE的自定义OAuth提供商📚
Multi-Session同一浏览器中支持多账户登录📚
API Key带速率限制的令牌认证📚

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
auth.api.*
bypass rate limiting.

内置可自定义规则的速率限制:
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 TypeUse CaseTradeoffs
Stateless (cookie-only)Read-heavy apps, edge/serverless, no revocation neededCan't revoke sessions, limited payload size
D1 DatabaseFull session management, audit trails, revocationEventual consistency issues
KV StorageStrong consistency, high read performanceExtra binding setup
Key Points:
  • Stateless sessions can't be revoked (user must wait for expiry)
  • Cookie size limit ~4KB (limits session data)
  • Use
    encoding: "jwt"
    for interoperability,
    "jwe"
    for encrypted
  • Server must have consistent
    BETTER_AUTH_SECRET
    across all instances

完全在签名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
    /api/auth/jwks
    for external services
  • 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,
    }),
  ],
});
核心要点
  • 密钥轮换可防止泄露的密钥长期有效
  • 临时保留旧密钥以验证现有令牌
  • 外部服务可通过
    /api/auth/jwks
    端点获取JWKS
  • 微服务场景使用RS256进行公钥验证
  • 单服务应用使用HS256(默认)

Provider Scopes Reference

提供商范围参考

Common OAuth providers and the scopes needed for user data:
ProviderScopeReturns
Google
openid
User ID only
email
Email address, email_verified
profile
Name, avatar (picture), locale
GitHub
user:email
Email address (may be private)
read:user
Name, avatar, profile URL, bio
Microsoft
openid
User ID only
email
Email address
profile
Name, locale
User.Read
Full profile from Graph API
Discord
identify
Username, avatar, discriminator
email
Email address
Apple
name
First/last name (first auth only)
email
Email or relay address
Patreon
identity
User ID, name
identity[email]
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提供商及获取用户数据所需的范围:
提供商范围返回数据
Google
openid
仅用户ID
email
邮箱地址、email_verified
profile
姓名、头像(picture)、地区
GitHub
user:email
邮箱地址(可能为私有)
read:user
姓名、头像、个人主页URL、简介
Microsoft
openid
仅用户ID
email
邮箱地址
profile
姓名、地区
User.Read
Graph API返回的完整个人资料
Discord
identify
用户名、头像、判别符
email
邮箱地址
Apple
name
名/姓(仅首次认证时返回)
email
邮箱或中继地址
Patreon
identity
用户ID、姓名
identity[email]
邮箱地址
Vercel(自动)邮箱、姓名、头像
配置示例
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:
StrategyFormatUse Case
Compact (default)Base64url + HMAC-SHA256Smallest, fastest
JWTStandard JWTInteroperable
JWEA256CBC-HS512 encryptedMost 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
freshAge
to control this window.

会话Cookie的三种编码策略:
策略格式适用场景
Compact(默认)Base64url + HMAC-SHA256体积最小、速度最快
JWT标准JWT互操作性强
JWEA256CBC-HS512加密安全性最高
typescript
export const auth = betterAuth({
  session: {
    cookieCache: {
      enabled: true,
      maxAge: 300, // 5分钟
      encoding: "compact", // 或"jwt"、"jwe"
    },
    freshAge: 60 * 60 * 24, // 1天 - 需要新鲜会话的操作
  },
});
新鲜会话:部分敏感操作要求使用最近创建的会话。配置
freshAge
控制此时间窗口。

New 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
undefined

wrangler.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:
create
,
update
for
user
,
session
,
account
,
verification
tables.

在数据库操作期间执行自定义逻辑:
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 });
        },
      },
    },
  },
});
可用钩子
user
session
account
verification
表的
create
update
操作。

Expo/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
auth.handler()
, better-auth automatically exposes 80+ production-ready REST endpoints at
/api/auth/*
. Every endpoint is also available as a server-side method via
auth.api.*
for programmatic use.
This dual-layer API system means:
  • Clients (React, Vue, mobile apps) call HTTP endpoints directly
  • Server-side code (middleware, background jobs) uses
    auth.api.*
    methods
  • 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.

调用
auth.handler()
后,better-auth会自动在
/api/auth/*
路径下暴露80+个生产就绪的REST端点。每个端点也可通过
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
/api/auth/*
when using
auth.handler()
.
使用
auth.handler()
时,所有端点会自动暴露在
/api/auth/*
路径下。

Core Authentication Endpoints

核心认证端点

EndpointMethodDescription
/sign-up/email
POSTRegister with email/password
/sign-in/email
POSTAuthenticate with email/password
/sign-out
POSTLogout user
/change-password
POSTUpdate password (requires current password)
/forget-password
POSTInitiate password reset flow
/reset-password
POSTComplete password reset with token
/send-verification-email
POSTSend email verification link
/verify-email
GETVerify email with token (
?token=<token>
)
/get-session
GETRetrieve current session
/list-sessions
GETGet all active user sessions
/revoke-session
POSTEnd specific session
/revoke-other-sessions
POSTEnd all sessions except current
/revoke-sessions
POSTEnd all user sessions
/update-user
POSTModify user profile (name, image)
/change-email
POSTUpdate email address
/set-password
POSTAdd password to OAuth-only account
/delete-user
POSTRemove user account
/list-accounts
GETGet linked authentication providers
/link-social
POSTConnect OAuth provider to account
/unlink-account
POSTDisconnect provider
端点方法描述
/sign-up/email
POST邮箱/密码注册
/sign-in/email
POST邮箱/密码认证
/sign-out
POST用户登出
/change-password
POST修改密码(需要当前密码)
/forget-password
POST启动密码重置流程
/reset-password
POST使用令牌完成密码重置
/send-verification-email
POST发送邮箱验证链接
/verify-email
GET使用令牌验证邮箱(
?token=<token>
/get-session
GET获取当前会话
/list-sessions
GET获取用户所有活跃会话
/revoke-session
POST终止指定会话
/revoke-other-sessions
POST终止除当前外的所有会话
/revoke-sessions
POST终止用户所有会话
/update-user
POST修改用户资料(姓名、头像)
/change-email
POST更新邮箱地址
/set-password
POST为仅使用OAuth的账户添加密码
/delete-user
POST删除用户账户
/list-accounts
GET获取关联的认证提供商
/link-social
POST关联OAuth提供商账户
/unlink-account
POST解除提供商关联

Social OAuth Endpoints

社交OAuth端点

EndpointMethodDescription
/sign-in/social
POSTInitiate OAuth flow (provider specified in body)
/callback/:provider
GETOAuth callback handler (e.g.,
/callback/google
)
/get-access-token
GETRetrieve 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

端点方法描述
/sign-in/social
POST启动OAuth流程(请求体中指定提供商)
/callback/:provider
GETOAuth回调处理程序(例如
/callback/google
/get-access-token
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";
EndpointMethodDescription
/two-factor/enable
POSTActivate 2FA for user
/two-factor/disable
POSTDeactivate 2FA
/two-factor/get-totp-uri
GETGet QR code URI for authenticator app
/two-factor/verify-totp
POSTValidate TOTP code from authenticator
/two-factor/send-otp
POSTSend OTP via email
/two-factor/verify-otp
POSTValidate email OTP
/two-factor/generate-backup-codes
POSTCreate recovery codes
/two-factor/verify-backup-code
POSTUse backup code for login
/two-factor/view-backup-codes
GETView current backup codes
typescript
import { twoFactor } from "better-auth/plugins";
端点方法描述
/two-factor/enable
POST为用户启用2FA
/two-factor/disable
POST为用户禁用2FA
/two-factor/get-totp-uri
GET获取认证器应用的二维码URI
/two-factor/verify-totp
POST验证认证器生成的TOTP码
/two-factor/send-otp
POST通过邮箱发送OTP
/two-factor/verify-otp
POST验证邮箱OTP
/two-factor/generate-backup-codes
POST创建恢复码
/two-factor/verify-backup-code
POST使用恢复码登录
/two-factor/view-backup-codes
GET查看当前恢复码
Organization Plugin (Multi-Tenant SaaS)
组织插件(多租户SaaS)
typescript
import { organization } from "better-auth/plugins";
Organizations (10 endpoints):
EndpointMethodDescription
/organization/create
POSTCreate organization
/organization/list
GETList user's organizations
/organization/get-full
GETGet complete org details
/organization/update
PUTModify organization
/organization/delete
DELETERemove organization
/organization/check-slug
GETVerify slug availability
/organization/set-active
POSTSet active organization context
Members (8 endpoints):
EndpointMethodDescription
/organization/list-members
GETGet organization members
/organization/add-member
POSTAdd member directly
/organization/remove-member
DELETERemove member
/organization/update-member-role
PUTChange member role
/organization/get-active-member
GETGet current member info
/organization/leave
POSTLeave organization
Invitations (7 endpoints):
EndpointMethodDescription
/organization/invite-member
POSTSend invitation email
/organization/accept-invitation
POSTAccept invite
/organization/reject-invitation
POSTReject invite
/organization/cancel-invitation
POSTCancel pending invite
/organization/get-invitation
GETGet invitation details
/organization/list-invitations
GETList org invitations
/organization/list-user-invitations
GETList user's pending invites
Teams (8 endpoints):
EndpointMethodDescription
/organization/create-team
POSTCreate team within org
/organization/list-teams
GETList organization teams
/organization/update-team
PUTModify team
/organization/remove-team
DELETERemove team
/organization/set-active-team
POSTSet active team context
/organization/list-team-members
GETList team members
/organization/add-team-member
POSTAdd member to team
/organization/remove-team-member
DELETERemove team member
Permissions & Roles (6 endpoints):
EndpointMethodDescription
/organization/has-permission
POSTCheck if user has permission
/organization/create-role
POSTCreate custom role
/organization/delete-role
DELETEDelete custom role
/organization/list-roles
GETList all roles
/organization/get-role
GETGet role details
/organization/update-role
PUTModify role permissions
typescript
import { organization } from "better-auth/plugins";
组织(10个端点):
端点方法描述
/organization/create
POST创建组织
/organization/list
GET列出用户所属组织
/organization/get-full
GET获取完整组织详情
/organization/update
PUT修改组织信息
/organization/delete
DELETE删除组织
/organization/check-slug
GET验证slug可用性
/organization/set-active
POST设置活跃组织上下文
成员(8个端点):
端点方法描述
/organization/list-members
GET获取组织成员
/organization/add-member
POST直接添加成员
/organization/remove-member
DELETE移除成员
/organization/update-member-role
PUT修改成员角色
/organization/get-active-member
GET获取当前成员信息
/organization/leave
POST退出组织
邀请(7个端点):
端点方法描述
/organization/invite-member
POST发送邀请邮件
/organization/accept-invitation
POST接受邀请
/organization/reject-invitation
POST拒绝邀请
/organization/cancel-invitation
POST取消待处理邀请
/organization/get-invitation
GET获取邀请详情
/organization/list-invitations
GET列出组织邀请
/organization/list-user-invitations
GET列出用户待处理邀请
团队(8个端点):
端点方法描述
/organization/create-team
POST在组织内创建团队
/organization/list-teams
GET列出组织团队
/organization/update-team
PUT修改团队信息
/organization/remove-team
DELETE删除团队
/organization/set-active-team
POST设置活跃团队上下文
/organization/list-team-members
GET列出团队成员
/organization/add-team-member
POST添加成员至团队
/organization/remove-team-member
DELETE从团队移除成员
权限与角色(6个端点):
端点方法描述
/organization/has-permission
POST检查用户是否拥有指定权限
/organization/create-role
POST创建自定义角色
/organization/delete-role
DELETE删除自定义角色
/organization/list-roles
GET列出所有角色
/organization/get-role
GET获取角色详情
/organization/update-role
PUT修改角色权限
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",
})
EndpointMethodDescription
/admin/create-user
POSTCreate user as admin
/admin/list-users
GETList all users (with filters/pagination)
/admin/set-role
POSTAssign user role
/admin/set-user-password
POSTChange user password
/admin/update-user
PUTModify user details
/admin/remove-user
DELETEDelete user account
/admin/ban-user
POSTBan user account (with optional expiry)
/admin/unban-user
POSTUnban user
/admin/list-user-sessions
GETGet user's active sessions
/admin/revoke-user-session
DELETEEnd specific user session
/admin/revoke-user-sessions
DELETEEnd all user sessions
/admin/impersonate-user
POSTStart impersonating user
/admin/stop-impersonating
POSTEnd impersonation session
⚠️ Breaking Change (v1.4.6):
allowImpersonatingAdmins
now defaults to
false
. Set to
true
explicitly if you need admin-on-admin impersonation.
Custom 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: "您的账户已被暂停",
})
端点方法描述
/admin/create-user
POST以管理员身份创建用户
/admin/list-users
GET列出所有用户(支持过滤/分页)
/admin/set-role
POST为用户分配角色
/admin/set-user-password
POST修改用户密码
/admin/update-user
PUT修改用户详情
/admin/remove-user
DELETE删除用户账户
/admin/ban-user
POST封禁用户账户(可选设置过期时间)
/admin/unban-user
POST解封用户
/admin/list-user-sessions
GET获取用户活跃会话
/admin/revoke-user-session
DELETE终止用户指定会话
/admin/revoke-user-sessions
DELETE终止用户所有会话
/admin/impersonate-user
POST开始模拟用户
/admin/stop-impersonating
POST结束模拟会话
⚠️ 重大变更(v1.4.6)
allowImpersonatingAdmins
默认值改为
false
。如果需要管理员间模拟,请显式设置为
true
带权限的自定义角色(v1.4.10)
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:
  • /token
    (get JWT),
    /jwks
    (public key for verification)
OpenAPI Plugin (2 endpoints) - Docs:
  • /reference
    (interactive API docs with Scalar UI)
  • /generate-openapi-schema
    (get OpenAPI spec as JSON)

Passkey插件(5个端点) - 文档
  • /passkey/add
    ,
    /sign-in/passkey
    ,
    /passkey/list
    ,
    /passkey/delete
    ,
    /passkey/update
Magic Link插件(2个端点) - 文档
  • /sign-in/magic-link
    ,
    /magic-link/verify
Username插件(2个端点) - 文档
  • /sign-in/username
    ,
    /username/is-available
Phone Number插件(5个端点) - 文档
  • /sign-in/phone-number
    ,
    /phone-number/send-otp
    ,
    /phone-number/verify
    ,
    /phone-number/request-password-reset
    ,
    /phone-number/reset-password
Email OTP插件(6个端点) - 文档
  • /email-otp/send-verification-otp
    ,
    /email-otp/check-verification-otp
    ,
    /sign-in/email-otp
    ,
    /email-otp/verify-email
    ,
    /forget-password/email-otp
    ,
    /email-otp/reset-password
Anonymous插件(1个端点) - 文档
  • /sign-in/anonymous
JWT插件(2个端点) - 文档
  • /token
    (获取JWT),
    /jwks
    (验证用公钥)
OpenAPI插件(2个端点) - 文档
  • /reference
    (交互式API文档,使用Scalar UI)
  • /generate-openapi-schema
    (获取OpenAPI规范JSON)

Server-Side API Methods (
auth.api.*
)

服务器端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 CaseUse HTTP EndpointsUse
auth.api.*
Methods
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端点使用
auth.api.*
方法
客户端认证✅ 是❌ 否
服务器中间件❌ 否✅ 是
后台任务❌ 否✅ 是
管理员面板✅ 是(客户端调用)✅ 是(服务器调用)
自定义认证流程❌ 否✅ 是
移动应用✅ 是❌ 否
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/reference
This 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
import { d1Adapter } from 'better-auth/adapters/d1'
but this doesn't exist.
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:
npx better-auth migrate
doesn't create D1-compatible schema.
Symptoms: Migration SQL has wrong syntax or doesn't work with D1.
Solution: Use Drizzle Kit to generate migrations:
bash
undefined
问题
npx better-auth migrate
无法创建兼容D1的schema。
症状:迁移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
问题:数据库中列名为
email_verified
,但better-auth期望
emailVerified
症状:会话读取失败,用户数据字段缺失。
⚠️ 关键提示(v1.4.10+):使用Kysely的
CamelCasePlugin
破坏better-auth适配器中的连接解析。该插件会将
_joined_user_user_id
这类连接键转换为
_joinedUserUserId
,导致会话查询中用户数据为null。
Drizzle解决方案:从一开始就使用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.

---
问题:写入后立即读取会话返回旧数据。
症状:用户登录后,下一次请求中
getSession()
返回null。
解决方案:使用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.toml
toml
[[kv_namespaces]]
binding = "SESSIONS_KV"
id = "your-kv-namespace-id"

Issue 3: "CamelCase" vs "snake_case" Column Mismatch

问题5:SPA应用CORS错误

Problem: Database has
email_verified
but better-auth expects
emailVerified
.
Symptoms: Session reads fail, user data missing fields.
⚠️ CRITICAL (v1.4.10+): Using Kysely's
CamelCasePlugin
breaks join parsing in better-auth adapter. The plugin converts join keys like
_joined_user_user_id
to
_joinedUserUserId
, causing user data to be null in session queries.
Solution 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" },
})

问题:认证API与前端不在同一源时出现CORS错误。
症状:浏览器控制台中出现
Access-Control-Allow-Origin
错误。
解决方案:在Worker中配置CORS头,并确保
trustedOrigins
匹配:
typescript
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
getSession()
returns null on next request.
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.toml
:
toml
[[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:
Access-Control-Allow-Origin
errors in browser console.
Solution: Configure CORS headers in Worker and ensure
trustedOrigins
match:
typescript
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)

问题:TypeScript错误或运行时错误提示缺少包。
症状
Cannot find module 'drizzle-orm'
或类似错误。
解决方案:安装所有必需的包:
Drizzle方案
bash
npm install better-auth drizzle-orm drizzle-kit @cloudflare/workers-types
Kysely方案
bash
npm install better-auth kysely kysely-d1 @cloudflare/workers-types

Issue 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 match
Check 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);

问题:邮箱验证链接始终未收到。
症状:用户注册后未收到邮件。
解决方案:实现
sendVerificationEmail
处理程序:
typescript
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:
Cannot find module 'drizzle-orm'
or similar.
Solution: Install all required packages:
For Drizzle approach:
bash
npm install better-auth drizzle-orm drizzle-kit @cloudflare/workers-types
For 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
sendVerificationEmail
handler:
typescript
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登录后
session.user.name
为null。
解决方案:请求额外的范围:
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:
session.user.name
is null after Google/GitHub sign-in.
Solution: 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 dev

Issue 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。调用
queryClient.invalidateQueries()
仅会使React Query缓存失效,不会影响better-auth nanostore。
解决方案:更新用户数据后手动通知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()
注意
$store
是未公开的内部API。此模式已在生产环境验证,但未来better-auth版本可能变更。
来源:社区发现的模式,已在生产环境使用

Issue 12: Wrangler Dev Mode Not Working

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

Problem:
wrangler dev
fails with database errors.
Symptoms: "Database not found" or migration errors in local dev.
Solution: Apply migrations locally first:
bash
undefined
问题:better-auth CLI(
npx @better-auth/cli generate
)在使用D1时失败,提示"Failed to initialize database adapter"。
症状: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不支持
    ALTER TABLE DROP COLUMN
    - 如果schema偏离,使用全新迁移模式(删除并重建表)
  • 在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插件方法如
listUsers
仍失败,提示"You are not allowed to list users"。
症状:自定义
requireAdmin
中间件(检查ADMIN_EMAILS环境变量)通过,但
auth.api.listUsers()
返回403。
根本原因:better-auth Admin插件有两层授权:
  1. 你的中间件 - 自定义检查(如ADMIN_EMAILS)
  2. better-auth内部 - 检查数据库中的
    user.role === 'admin'
必须同时通过两层检查才能使用Admin插件方法。
解决方案:在数据库中将用户角色设置为'admin':
sql
// 修复现有用户
UPDATE user SET role = 'admin' WHERE email = 'admin@example.com';
或在初始设置后使用Admin UI/API设置角色。
原因:Admin插件的
listUsers
banUser
impersonateUser
等方法都会检查数据库中的
user.role
,而非你的自定义中间件逻辑。
来源:生产环境调试 - 误导性错误信息通过
wrangler tail
发现根本原因

Then run dev server

问题16:Organization/Team的updated_at必须为可空

wrangler dev

---
问题:尽管API返回"slug already exists",但组织创建仍因SQL约束错误失败。
症状
  • 错误信息显示"An organization with this slug already exists"
  • 数据库表实际为空
  • wrangler tail
    显示:
    Failed query: insert into "organization" ... values (?, ?, ?, null, null, ?, null)
根本原因:better-auth在创建时为
updated_at
插入
null
(仅在更新时设置)。如果你的schema设置了
NOT NULL
约束,插入会失败。
解决方案:在schema和迁移中使
updated_at
为可空:
typescript
// 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
);
适用范围
organization
team
表(可能包括其他插件表)
来源:生产环境调试 -
wrangler tail
揭示了"slug已存在"误导性信息背后的实际SQL错误

Issue 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
useSession()
despite calling
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
queryClient.invalidateQueries()
only invalidates React Query cache, not the better-auth nanostore.
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
refetch()
from
useSession()
, but
$store.notify()
is more direct:
typescript
const { data: session, refetch } = authClient.useSession()
// After update
await refetch()
Note:
$store
is an undocumented internal API. This pattern is production-validated but may change in future better-auth versions.
Source: 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 (
npx @better-auth/cli generate
) fails with "Failed to initialize database adapter" when using D1.
Symptoms: 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
    snake_case
    (e.g.,
    rate_limit_time_window
    , not
    rateLimitTimeWindow
    )
  • D1 doesn't support
    ALTER TABLE DROP COLUMN
    - if schema drifts, use fresh migration pattern (drop and recreate tables)
  • In Drizzle adapter config, use
    apikey
    (lowercase) as the table name mapping
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/client
导入
expoClient
时崩溃,提示
TypeError: 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
listUsers
fail with "You are not allowed to list users" even though your middleware passes.
Symptoms: Custom
requireAdmin
middleware (checking ADMIN_EMAILS env var) passes, but
auth.api.listUsers()
returns 403.
Root Cause: better-auth admin plugin has two authorization layers:
  1. Your middleware - Custom check (e.g., ADMIN_EMAILS)
  2. better-auth internal - Checks
    user.role === 'admin'
    in database
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
listUsers
,
banUser
,
impersonateUser
, etc. all check
user.role
in the database, not your custom middleware logic.
Source: Production debugging - misleading error message led to root cause discovery via
wrangler tail

问题:v1.4.12后,
additionalFields
type: 'string[]'
的字段在通过Drizzle直接查询时返回JSON字符串(
'["a","b"]'
)而非原生数组。
症状
user.notificationTokens
是字符串而非数组。期望数组的代码崩溃。
根本原因:在Drizzle适配器中,
string[]
字段使用
mode: 'json'
存储,期望传入数组。但better-auth v1.4.4+向Drizzle传入字符串,导致双重序列化。通过Drizzle直接查询时,值为字符串,但使用**better-auth
internalAdapter
**时,转换器会正确返回数组。
解决方案
  1. **使用better-auth
    internalAdapter
    **而非直接查询Drizzle(内置转换器)
  2. 修改Drizzle schema,将string[]字段改为
    .jsonb()
  3. 手动解析JSON字符串直至修复
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
  • wrangler tail
    shows:
    Failed query: insert into "organization" ... values (?, ?, ?, null, null, ?, null)
Root Cause: better-auth inserts
null
for
updated_at
on creation (only sets it on updates). If your schema has
NOT NULL
constraint, insert fails.
Solution: Make
updated_at
nullable in both schema and migrations:
typescript
// 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:
organization
and
team
tables (possibly other plugin tables)
Source: Production debugging -
wrangler tail
revealed actual SQL error behind misleading "slug exists" message

问题:在
additionalFields
上设置
returned: false
会阻止字段通过API保存,即使设置了
input: true
症状:通过API端点创建/更新时,字段从未保存至数据库。
根本原因
returned: false
属性会同时阻止读取和写入操作,而非仅读取。
input: 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
listMembers
return
{ members: [...], total: N }
. Wrapping with
c.json({ members: result })
creates double nesting.
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
    { organizations: [...] }
    (check structure)
  • 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.freshAge
检查的是会话创建后的时间,而非最近活动时间。即使持续使用,活跃会话在
freshAge
过后也会变为"不新鲜"。
症状:需要"新鲜会话"的端点拒绝有效的活跃会话。
原因
freshSessionMiddleware
检查
Date.now() - (session.updatedAt || session.createdAt)
,但
updatedAt
仅在基于
updateAge
刷新会话时才会更新。如果
updateAge > freshAge
,会话会在
updatedAt
更新前变为"不新鲜"。
解决方案
  1. 设置
    updateAge <= freshAge
    ,确保在过期前更新会话新鲜度
  2. 避免对长会话使用"新鲜会话"要求
  3. 接受设计: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
expoClient
from
@better-auth/expo/client
crashes with
TypeError: Cannot read property 'fromJSONSchema' of undefined
on v1.4.16.
Symptoms: 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
    f4a9f15
    (pre-regression)
  • 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 release

问题:OAuth 2.1和OIDC令牌端点返回
{ "response": { ...tokens... } }
而非符合规范的顶层JSON。OAuth客户端期望
{ "access_token": "...", "token_type": "bearer" }
在顶层。
症状:OAuth客户端失败,提示
Bearer undefined
invalid_token
根本原因:端点流水线返回
{ response, headers, status }
供内部使用,直接序列化为HTTP响应。这违反了OAuth/OIDC规范要求。
解决方案
  • 临时方案:在客户端手动解析
    .response
    字段
  • 永久方案:等待修复(Issue #7355已开放,接受贡献)
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,
additionalFields
with
type: 'string[]'
return stringified arrays (
'["a","b"]'
) instead of native arrays when querying via Drizzle directly.
Symptoms:
user.notificationTokens
is a string, not an array. Code expecting arrays breaks.
Root Cause: In Drizzle adapter,
string[]
fields are stored with
mode: 'json'
, 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
internalAdapter
, a transformer correctly returns an array.
Solution:
  1. Use better-auth
    internalAdapter
    instead of querying Drizzle directly (has transformer)
  2. Change Drizzle schema to
    .jsonb()
    for string[] fields
  3. 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)

核心差异
  • Clerk:第三方服务 → better-auth:自托管
  • Clerk:专有 → better-auth:开源
  • Clerk:按月付费 → better-auth:免费
迁移步骤
  1. 导出用户数据:从Clerk导出(CSV或API)
  2. 导入至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,
      });
    }
  3. 替换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;
  4. 更新中间件用于会话验证
  5. 配置社交提供商(使用相同的OAuth应用,不同的配置)

Issue 20: additionalFields "returned" Property Blocks Input

从Auth.js(NextAuth)迁移

Problem: Setting
returned: false
on
additionalFields
prevents field from being saved via API, even with
input: true
.
Symptoms: Field never saved to database when creating/updating via API endpoints.
Root Cause: The
returned: false
property blocks both read AND write operations, not just reads as intended. The
input: true
property should control write access independently.
Solution:
  • Don't use
    returned: false
    if you need API write access
  • Write via server-side methods (
    auth.api.*
    ) instead
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 database

核心差异
  • Auth.js:功能有限 → better-auth:功能全面(2FA、组织等)
  • Auth.js:回调密集 → better-auth:插件化
  • Auth.js:会话处理不一致 → better-auth:一致
迁移步骤
  1. 数据库schema:Auth.js和better-auth schema相似,但列名不同
  2. 替换配置
    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: { /* ... */ },
      },
    });
  3. 更新客户端钩子
    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:
session.freshAge
checks time-since-creation, NOT recent activity. Active sessions become "not fresh" after
freshAge
elapses, even if used constantly.
Symptoms: "Fresh session required" endpoints reject valid active sessions.
Why It Happens: The
freshSessionMiddleware
checks
Date.now() - (session.updatedAt || session.createdAt)
, but
updatedAt
only changes when the session is refreshed based on
updateAge
. If
updateAge > freshAge
, the session becomes "not fresh" before
updatedAt
is bumped.
Solution:
  1. Set
    updateAge <= freshAge
    to ensure freshness is updated before expiry
  2. Avoid "fresh session required" gating for long-lived sessions
  3. 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 session

Issue 22: OAuth Token Endpoints Return Wrapped JSON

核心概念

Problem: OAuth 2.1 and OIDC token endpoints return
{ "response": { ...tokens... } }
instead of spec-compliant top-level JSON. OAuth clients expect
{ "access_token": "...", "token_type": "bearer" }
at root.
Symptoms: OAuth clients fail with
Bearer undefined
or
invalid_token
.
Root Cause: The endpoint pipeline returns
{ response, headers, status }
for internal use, which gets serialized directly for HTTP requests. This breaks OAuth/OIDC spec requirements.
Solution:
  • Temporary: Manually unwrap
    .response
    field on client
  • 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`

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:
  1. Export user data from Clerk (CSV or API)
  2. 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,
      });
    }
  3. 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;
  4. Update middleware for session verification
  5. 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:
  1. Database schema: Auth.js and better-auth use similar schemas, but column names differ
  2. 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: { /* ... */ },
      },
    });
  3. Update client hooks:
    typescript
    // Before
    import { useSession } from "next-auth/react";
    
    // After
    import { authClient } from "@/lib/auth-client";
    const { data: session } = authClient.useSession();

Additional Resources

框架集成

Official Documentation

Core Concepts

社区与支持

Authentication Methods

相关文档

Plugin Documentation

生产示例

已验证的D1仓库(均使用Drizzle或Kysely):
  1. zpg6/better-auth-cloudflare - Drizzle + D1(包含CLI)
  2. zwily/example-react-router-cloudflare-d1-drizzle-better-auth - Drizzle + D1
  3. foxlau/react-router-v7-better-auth - Drizzle + D1
  4. matthewlynch/better-auth-react-router-cloudflare-d1 - Kysely + D1
没有仓库使用直接的
d1Adapter
- 均需搭配Drizzle/Kysely。

Framework Integrations

版本兼容性

已测试版本
  • better-auth@1.4.10
  • drizzle-orm@0.45.1
  • drizzle-kit@0.31.8
  • kysely@0.28.9
  • kysely-d1@0.4.0
  • @cloudflare/workers-types@latest
  • hono@4.11.3
  • Node.js 18+, Bun 1.0+
重大变更
  • v1.4.6:
    allowImpersonatingAdmins
    默认值改为
    false
  • v1.4.0:仅支持ESM(无CommonJS)
  • v1.3.0:多团队表结构变更

Community & Support

社区资源

Cloudflare特定指南

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

Production Examples

Verified working D1 repositories (all use Drizzle or Kysely):
  1. zpg6/better-auth-cloudflare - Drizzle + D1 (includes CLI)
  2. zwily/example-react-router-cloudflare-d1-drizzle-better-auth - Drizzle + D1
  3. foxlau/react-router-v7-better-auth - Drizzle + D1
  4. matthewlynch/better-auth-react-router-cloudflare-d1 - Kysely + D1
None use a direct
d1Adapter
- all require Drizzle/Kysely.

Version Compatibility

Tested with:
  • better-auth@1.4.10
  • drizzle-orm@0.45.1
  • drizzle-kit@0.31.8
  • kysely@0.28.9
  • kysely-d1@0.4.0
  • @cloudflare/workers-types@latest
  • hono@4.11.3
  • Node.js 18+, Bun 1.0+
Breaking changes:
  • v1.4.6:
    allowImpersonatingAdmins
    defaults to
    false
  • v1.4.0: ESM-only (no CommonJS)
  • v1.3.0: Multi-team table structure change

Community Resources

Cloudflare-specific guides:

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.