better-auth-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Better Auth Best Practices

Better Auth 最佳实践

Comprehensive reference for the Better Auth framework. Covers configuration, security hardening, rate limiting, session management, plugins, and production deployment patterns.
Canonical docs: better-auth.com/docs Source: Synthesized from better-auth/skills (official upstream) + Grove production experience.
这是Better Auth框架的综合参考手册,涵盖配置、安全加固、速率限制、会话管理、插件及生产环境部署模式。
官方文档: better-auth.com/docs 资料来源: 综合自better-auth/skills(官方上游)+ Grove生产环境实践经验。

When to Activate

适用场景

  • Configuring or modifying Better Auth server/client setup
  • Auditing auth security (pair with
    raccoon-audit
    ,
    turtle-harden
    )
  • Adding or configuring rate limiting
  • Setting up session management, cookie caching, or secondary storage
  • Adding plugins (2FA, organizations, passkeys, etc.)
  • Troubleshooting auth issues on Heartwood or any Better Auth deployment
  • Reviewing security posture before production deploy
Pair with:
heartwood-auth
(Grove-specific integration),
spider-weave
(auth architecture),
turtle-harden
(deep security)

  • 配置或修改Better Auth服务端/客户端设置
  • 审核认证安全性(可搭配
    raccoon-audit
    turtle-harden
    使用)
  • 添加或配置速率限制
  • 设置会话管理、Cookie缓存或二级存储
  • 添加插件(双因素认证、组织管理、密钥登录等)
  • 排查Heartwood或任何Better Auth部署中的认证问题
  • 生产环境部署前审查安全状态
搭配技能:
heartwood-auth
(Grove专属集成方案)、
spider-weave
(认证架构)、
turtle-harden
(深度安全加固)

Grove Context: Heartwood

Grove 专属上下文:Heartwood

Heartwood is Grove's auth service, powered by Better Auth on Cloudflare Workers.
ComponentDetail
Frontend
heartwood.grove.place
API
auth-api.grove.place
DatabaseCloudflare D1 (SQLite)
Session cacheCloudflare KV (
SESSION_KV
)
ProvidersGoogle OAuth, Magic Links, Passkeys
Cookie domain
.grove.place
(cross-subdomain SSO)
Everything in this skill applies directly to Heartwood. The
heartwood-auth
skill covers Grove-specific integration patterns (client setup, route protection, error codes). This skill covers the framework itself.

Heartwood是Grove的认证服务,基于Cloudflare Workers上的Better Auth构建。
组件详情
前端
heartwood.grove.place
API
auth-api.grove.place
数据库Cloudflare D1(SQLite)
会话缓存Cloudflare KV(
SESSION_KV
登录提供商Google OAuth、魔法链接、密钥登录
Cookie域名
.grove.place
(跨子域单点登录)
本技能中的所有内容均直接适用于Heartwood。
heartwood-auth
技能涵盖Grove专属的集成模式(客户端设置、路由保护、错误码),而本技能聚焦框架本身。

Quick Reference

快速参考

Environment Variables

环境变量

VariablePurpose
BETTER_AUTH_SECRET
Encryption secret (min 32 chars). Generate:
openssl rand -base64 32
BETTER_AUTH_URL
Base URL (e.g.,
https://auth-api.grove.place
)
BETTER_AUTH_TRUSTED_ORIGINS
Comma-separated trusted origins
Only define
baseURL
/
secret
in config if env vars are NOT set.
变量用途
BETTER_AUTH_SECRET
加密密钥(至少32字符)。生成方式:
openssl rand -base64 32
BETTER_AUTH_URL
基础URL(例如:
https://auth-api.grove.place
BETTER_AUTH_TRUSTED_ORIGINS
逗号分隔的可信源列表
仅当未设置环境变量时,才在配置中定义
baseURL
/
secret

File Location

文件位置

CLI looks for
auth.ts
in:
./
,
./lib
,
./utils
, or under
./src
. Use
--config
for custom path.
CLI会在以下目录查找
auth.ts
./
./lib
./utils
./src
下。使用
--config
指定自定义路径。

CLI Commands

CLI命令

bash
npx @better-auth/cli@latest migrate    # Apply schema (built-in adapter)
npx @better-auth/cli@latest generate   # Generate schema for Prisma/Drizzle
Re-run after adding/changing plugins.

bash
npx @better-auth/cli@latest migrate    # 应用数据库 schema(内置适配器)
npx @better-auth/cli@latest generate   # 为Prisma/Drizzle生成schema
添加或修改插件后请重新运行上述命令。

Core Configuration

核心配置

OptionNotes
appName
Display name (used in 2FA issuer, emails)
baseURL
Only if
BETTER_AUTH_URL
not set
basePath
Default
/api/auth
. Set
/
for root
secret
Only if
BETTER_AUTH_SECRET
not set
database
Required. Connection or adapter instance
secondaryStorage
Redis/KV for sessions & rate limits
emailAndPassword
{ enabled: true }
to activate
socialProviders
{ google: { clientId, clientSecret }, ... }
plugins
Array of plugins
trustedOrigins
CSRF whitelist (baseURL auto-trusted)

选项说明
appName
显示名称(用于双因素认证发行方、邮件内容)
baseURL
仅当未设置
BETTER_AUTH_URL
时使用
basePath
默认值
/api/auth
。设置为
/
表示根路径
secret
仅当未设置
BETTER_AUTH_SECRET
时使用
database
必填项。数据库连接或适配器实例
secondaryStorage
用于会话和速率限制的Redis/KV存储
emailAndPassword
设置
{ enabled: true }
以启用邮箱密码登录
socialProviders
配置示例:
{ google: { clientId, clientSecret }, ... }
plugins
插件数组
trustedOrigins
CSRF白名单(baseURL会自动加入可信列表)

Database

数据库

Direct connections: Pass
pg.Pool
,
mysql2
pool,
better-sqlite3
, or
bun:sqlite
instance.
ORM adapters: Import from
better-auth/adapters/drizzle
,
better-auth/adapters/prisma
,
better-auth/adapters/mongodb
.
Critical gotcha: Better Auth uses adapter model names, NOT underlying table names. If Prisma model is
User
mapping to table
users
, use
modelName: "user"
(Prisma reference), not
"users"
.

直接连接: 传入
pg.Pool
mysql2
连接池、
better-sqlite3
bun:sqlite
实例。
ORM适配器:
better-auth/adapters/drizzle
better-auth/adapters/prisma
better-auth/adapters/mongodb
导入。
关键注意事项: Better Auth使用适配器模型名称,而非底层数据库表名。如果Prisma模型为
User
,对应数据库表为
users
,则需使用
modelName: "user"
(参考Prisma命名规则),而非
"users"

Rate Limiting

速率限制

Better Auth has built-in rate limiting — enabled by default in production, disabled in development.
Better Auth内置速率限制功能——生产环境默认启用,开发环境默认禁用。

Why This Matters for Grove

对Grove的重要性

Better Auth's rate limiter can replace custom threshold SDKs for auth endpoints. It's battle-tested, configurable per-endpoint, and integrates directly with the auth layer where it matters most.
Better Auth的速率限制器可替代认证端点的自定义阈值SDK。它经过实战检验,支持按端点配置,且直接集成在最关键的认证层。

Default Configuration

默认配置

typescript
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  rateLimit: {
    enabled: true,       // Default: true in production
    window: 10,          // Time window in seconds (default: 10)
    max: 100,            // Max requests per window (default: 100)
  },
});
typescript
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  rateLimit: {
    enabled: true,       // 默认:生产环境启用
    window: 10,          // 时间窗口(秒),默认值:10
    max: 100,            // 窗口内最大请求数,默认值:100
  },
});

Storage Options

存储选项

typescript
rateLimit: {
  storage: "secondary-storage", // Best for production
}
StorageBehavior
"memory"
Fast, resets on restart. Not recommended for serverless.
"database"
Persistent, adds DB load
"secondary-storage"
Uses configured KV/Redis. Default when available.
For Heartwood: Use
"secondary-storage"
backed by Cloudflare KV.
typescript
rateLimit: {
  storage: "secondary-storage", // 生产环境最佳选择
}
存储方式特性
"memory"
速度快,但重启后重置。不推荐用于无服务器环境。
"database"
持久化存储,但会增加数据库负载
"secondary-storage"
使用已配置的KV/Redis存储。当可用时为默认选项。
针对Heartwood: 使用基于Cloudflare KV的
"secondary-storage"

Per-Endpoint Rules

按端点自定义规则

Better Auth applies stricter defaults to sensitive endpoints:
  • /sign-in
    ,
    /sign-up
    ,
    /change-password
    ,
    /change-email
    : 3 requests per 10 seconds
Override for specific paths:
typescript
rateLimit: {
  customRules: {
    "/api/auth/sign-in/email": {
      window: 60,  // 1 minute
      max: 5,      // 5 attempts
    },
    "/api/auth/sign-up/email": {
      window: 60,
      max: 3,      // Very strict for registration
    },
    "/api/auth/some-safe-endpoint": false, // Disable rate limiting
  },
}
Better Auth对敏感端点应用更严格的默认限制:
  • /sign-in
    /sign-up
    /change-password
    /change-email
    10秒内最多3次请求
可针对特定路径覆盖默认规则:
typescript
rateLimit: {
  customRules: {
    "/api/auth/sign-in/email": {
      window: 60,  // 1分钟
      max: 5,      // 最多5次尝试
    },
    "/api/auth/sign-up/email": {
      window: 60,
      max: 3,      // 注册接口限制更严格
    },
    "/api/auth/some-safe-endpoint": false, // 禁用该端点的速率限制
  },
}

Custom Storage

自定义存储

For non-standard backends:
typescript
rateLimit: {
  customStorage: {
    get: async (key) => {
      // Return { count: number, expiresAt: number } or null
    },
    set: async (key, data) => {
      // Store the rate limit data
    },
  },
}
Each plugin can optionally define its own rate-limit rules per endpoint.

针对非标准后端:
typescript
rateLimit: {
  customStorage: {
    get: async (key) => {
      // 返回 { count: number, expiresAt: number } 或 null
    },
    set: async (key, data) => {
      // 存储速率限制数据
    },
  },
}
每个插件可选择性地为其端点定义专属速率限制规则。

Session Management

会话管理

Storage Priority

存储优先级

  1. If
    secondaryStorage
    defined → sessions go there (not DB)
  2. Set
    session.storeSessionInDatabase: true
    to also persist to DB
  3. No database +
    cookieCache
    → fully stateless mode
  1. 若配置了
    secondaryStorage
    → 会话存储在此处(而非数据库)
  2. 设置
    session.storeSessionInDatabase: true
    可同时将会话持久化到数据库
  3. 无数据库 + 启用
    cookieCache
    → 完全无状态模式

Key Options

关键配置选项

typescript
session: {
  expiresIn: 60 * 60 * 24 * 7,   // 7 days (default)
  updateAge: 60 * 60 * 24,        // Refresh every 24 hours (default)
  freshAge: 60 * 60 * 24,         // 24 hours for sensitive actions (default)
}
freshAge
— defines how recently a user must have authenticated to perform sensitive operations. Use to require re-auth for password changes, viewing sensitive data, etc.
typescript
session: {
  expiresIn: 60 * 60 * 24 * 7,   // 7天(默认值)
  updateAge: 60 * 60 * 24,        // 每24小时刷新一次(默认值)
  freshAge: 60 * 60 * 24,         // 敏感操作要求会话有效期不超过24小时(默认值)
}
freshAge
—— 定义用户必须在多长时间内完成过认证,才能执行敏感操作。可用于要求用户重新认证以修改密码、查看敏感数据等场景。

Cookie Cache Strategies

Cookie缓存策略

Cache session data in cookies to reduce DB/KV queries:
typescript
session: {
  cookieCache: {
    enabled: true,
    maxAge: 60 * 5,        // 5 minutes
    strategy: "compact",   // Options: "compact", "jwt", "jwe"
    version: 1,            // Change to invalidate all sessions
  },
}
StrategyDescription
compact
Base64url + HMAC. Smallest size. Default.
jwt
Standard HS256 JWT. Readable but signed.
jwe
A256CBC-HS512 encrypted. Maximum security.
Gotcha: Custom session fields are NOT cached — they're always re-fetched from storage.

在Cookie中缓存会话数据,减少数据库/KV查询:
typescript
session: {
  cookieCache: {
    enabled: true,
    maxAge: 60 * 5,        // 5分钟
    strategy: "compact",   // 可选值:"compact"、"jwt"、"jwe"
    version: 1,            // 修改版本号可使所有会话失效
  },
}
策略描述
compact
Base64url + HMAC加密。体积最小,为默认选项。
jwt
标准HS256 JWT格式。内容可读但已签名。
jwe
A256CBC-HS512加密。安全性最高。
注意事项: 自定义会话字段不会被缓存——始终会从存储中重新获取。

Security Configuration

安全配置

Secret Management

密钥管理

Better Auth looks for secrets in order:
  1. options.secret
    in config
  2. BETTER_AUTH_SECRET
    env var
  3. AUTH_SECRET
    env var
Requirements:
  • Rejects default/placeholder secrets in production
  • Warns if shorter than 32 characters
  • Warns if entropy below 120 bits
Better Auth按以下顺序查找密钥:
  1. 配置中的
    options.secret
  2. 环境变量
    BETTER_AUTH_SECRET
  3. 环境变量
    AUTH_SECRET
要求:
  • 生产环境中拒绝使用默认/占位符密钥
  • 若密钥长度不足32字符会发出警告
  • 若密钥熵值低于120位会发出警告

CSRF Protection

CSRF防护

Multi-layered by default:
  1. Origin header validation
    Origin
    /
    Referer
    must match trusted origins
  2. Fetch metadata — Uses
    Sec-Fetch-Site
    ,
    Sec-Fetch-Mode
    ,
    Sec-Fetch-Dest
    headers
  3. First-login protection — Validates origin even without cookies
typescript
advanced: {
  disableCSRFCheck: false,  // KEEP THIS FALSE
}
默认启用多层防护:
  1. Origin头验证 ——
    Origin
    /
    Referer
    必须匹配可信源
  2. Fetch元数据 —— 使用
    Sec-Fetch-Site
    Sec-Fetch-Mode
    Sec-Fetch-Dest
  3. 首次登录防护 —— 即使无Cookie也会验证Origin
typescript
advanced: {
  disableCSRFCheck: false,  // 保持为false
}

Trusted Origins

可信源配置

typescript
trustedOrigins: [
  "https://app.grove.place",
  "https://*.grove.place",          // Wildcard subdomain
  "exp://192.168.*.*:*/*",          // Custom schemes (Expo)
]
Dynamic computation:
typescript
trustedOrigins: async (request) => {
  const tenant = getTenantFromRequest(request);
  return [`https://${tenant}.grove.place`];
}
Validated parameters:
callbackURL
,
redirectTo
,
errorCallbackURL
,
newUserCallbackURL
,
origin
, and more. Invalid URLs get 403.
typescript
trustedOrigins: [
  "https://app.grove.place",
  "https://*.grove.place",          // 通配符子域名
  "exp://192.168.*.*:*/*",          // 自定义协议(如Expo)
]
动态计算可信源:
typescript
trustedOrigins: async (request) => {
  const tenant = getTenantFromRequest(request);
  return [`https://${tenant}.grove.place`];
}
验证参数包括:
callbackURL
redirectTo
errorCallbackURL
newUserCallbackURL
origin
等。无效URL会返回403。

Cookie Security

Cookie安全

Defaults are secure:
  • secure: true
    when baseURL uses HTTPS or in production
  • sameSite: "lax"
    (CSRF prevention while allowing navigation)
  • httpOnly: true
    (no JavaScript access)
  • __Secure-
    prefix when secure is enabled
typescript
advanced: {
  useSecureCookies: true,
  cookiePrefix: "better-auth",
  defaultCookieAttributes: {
    sameSite: "lax",
  },
  crossSubDomainCookies: {
    enabled: true,
    domain: ".grove.place",  // Note the leading dot
    additionalCookies: ["session_token", "session_data"],
  },
}
Warning: Cross-subdomain cookies expand attack surface. Only enable if you trust all subdomains.
默认配置已具备安全性:
  • 当baseURL使用HTTPS或在生产环境中,
    secure: true
  • sameSite: "lax"
    (防止CSRF同时允许正常导航)
  • httpOnly: true
    (禁止JavaScript访问)
  • 启用secure时自动添加
    __Secure-
    前缀
typescript
advanced: {
  useSecureCookies: true,
  cookiePrefix: "better-auth",
  defaultCookieAttributes: {
    sameSite: "lax",
  },
  crossSubDomainCookies: {
    enabled: true,
    domain: ".grove.place",  // 注意开头的点
    additionalCookies: ["session_token", "session_data"],
  },
}
警告: 跨子域Cookie会扩大攻击面。仅当信任所有子域时才启用。

IP-Based Security

基于IP的安全

typescript
advanced: {
  ipAddress: {
    ipAddressHeaders: ["x-forwarded-for", "x-real-ip"],
    ipv6Subnet: 64,            // Group IPv6 by /64
    disableIpTracking: false,  // Keep enabled for rate limiting
  },
  trustedProxyHeaders: true,   // Only if behind a trusted proxy
}
typescript
advanced: {
  ipAddress: {
    ipAddressHeaders: ["x-forwarded-for", "x-real-ip"],
    ipv6Subnet: 64,            // 按/64子网分组IPv6地址
    disableIpTracking: false,  // 保持启用以支持速率限制
  },
  trustedProxyHeaders: true,   // 仅当部署在可信代理后时启用
}

Background Tasks (Timing Attack Prevention)

后台任务(防止时序攻击)

Sensitive operations should complete in constant time. The
handler
callback receives a promise that must outlive the response — on serverless platforms, you need the platform's
waitUntil
to keep it alive.
Cloudflare Workers: Capture
ExecutionContext
from the fetch handler and close over it:
typescript
// In your Worker fetch handler:
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    // Create auth with ctx in scope
    const auth = createAuth(env, ctx);
    return auth.handler(request);
  },
};

// In your auth config factory:
function createAuth(env: Env, ctx: ExecutionContext) {
  return betterAuth({
    // ...
    advanced: {
      backgroundTasks: {
        handler: (promise) => ctx.waitUntil(promise),
      },
    },
  });
}
Vercel/Next.js: Use the
waitUntil
export from
@vercel/functions
:
typescript
import { waitUntil } from "@vercel/functions";

advanced: {
  backgroundTasks: {
    handler: (promise) => waitUntil(promise),
  },
}
Ensures email sending doesn't leak information about whether a user exists.
敏感操作应在固定时间内完成。
handler
回调会接收一个Promise,该Promise必须在响应完成后继续执行——在无服务器平台上,需使用平台的
waitUntil
方法保持其存活。
Cloudflare Workers: 从fetch处理函数中捕获
ExecutionContext
并在闭包中使用:
typescript
// 在Worker的fetch处理函数中:
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    // 创建auth实例并传入ctx
    const auth = createAuth(env, ctx);
    return auth.handler(request);
  },
};

// 在auth配置工厂函数中:
function createAuth(env: Env, ctx: ExecutionContext) {
  return betterAuth({
    // ...
    advanced: {
      backgroundTasks: {
        handler: (promise) => ctx.waitUntil(promise),
      },
    },
  });
}
Vercel/Next.js: 使用
@vercel/functions
导出的
waitUntil
typescript
import { waitUntil } from "@vercel/functions";

advanced: {
  backgroundTasks: {
    handler: (promise) => waitUntil(promise),
  },
}
确保邮件发送操作不会泄露用户是否存在的信息。

Account Enumeration Prevention

账户枚举防护

Built-in protections:
  1. Consistent response messages — Password reset always returns generic message
  2. Dummy operations — When user isn't found, still performs token generation + DB lookups
  3. Background email sending — Async to prevent timing differences

内置防护机制:
  1. 统一响应信息 —— 密码重置请求始终返回通用提示
  2. 虚拟操作 —— 当用户不存在时,仍会执行令牌生成和数据库查询操作
  3. 后台发送邮件 —— 异步执行以避免时序差异

OAuth / Social Provider Security

OAuth / 社交登录提供商安全

PKCE (Automatic)

PKCE(自动启用)

Better Auth automatically uses PKCE for all OAuth flows:
  1. Generates 128-character random
    code_verifier
  2. Creates
    code_challenge
    using S256 (SHA-256)
  3. Validates code exchange with original verifier
Better Auth自动为所有OAuth流程使用PKCE:
  1. 生成128字符的随机
    code_verifier
  2. 使用S256(SHA-256)生成
    code_challenge
  3. 使用原始verifier验证代码交换

State Parameter

State参数

typescript
account: {
  storeStateStrategy: "cookie",  // "cookie" (default) or "database"
}
State tokens: 32-character random strings, expire after 10 minutes, contain encrypted callback URLs + PKCE verifier.
typescript
account: {
  storeStateStrategy: "cookie",  // "cookie"(默认)或 "database"
}
State令牌:32字符随机字符串,10分钟后过期,包含加密的回调URL + PKCE verifier。

Encrypt Stored OAuth Tokens

加密存储OAuth令牌

typescript
account: {
  encryptOAuthTokens: true,  // AES-256-GCM
}
Enable if you store OAuth tokens for API access on behalf of users.

typescript
account: {
  encryptOAuthTokens: true,  // AES-256-GCM加密
}
如果需要存储OAuth令牌以代表用户访问API,请启用此选项。

Email & Password

邮箱与密码

Email Verification

邮箱验证

typescript
emailVerification: {
  sendVerificationEmail: async ({ user, url }) => {
    await sendEmail({ to: user.email, subject: "Verify your email", url });
  },
  sendOnSignUp: true,
  requireEmailVerification: true,  // Blocks sign-in until verified
}
typescript
emailVerification: {
  sendVerificationEmail: async ({ user, url }) => {
    await sendEmail({ to: user.email, subject: "验证你的邮箱", url });
  },
  sendOnSignUp: true,
  requireEmailVerification: true,  // 验证完成前阻止登录
}

Password Reset

密码重置

typescript
emailAndPassword: {
  sendResetPassword: async ({ user, url }) => {
    await sendEmail({ to: user.email, subject: "Reset your password", url });
  },
  password: {
    minLength: 8,   // Default
    maxLength: 128,  // Default
  },
  revokeSessionsOnPasswordReset: true,  // Log out all sessions
}
Security: Reset tokens are 24-character alphanumeric strings, expire after 1 hour, single-use.
typescript
emailAndPassword: {
  sendResetPassword: async ({ user, url }) => {
    await sendEmail({ to: user.email, subject: "重置你的密码", url });
  },
  password: {
    minLength: 8,   // 默认值
    maxLength: 128,  // 默认值
  },
  revokeSessionsOnPasswordReset: true,  // 密码重置后登出所有会话
}
安全性: 重置令牌为24字符字母数字组合,1小时后过期,仅可使用一次。

Password Hashing

密码哈希

Default: scrypt. For Argon2id, provide custom
hash
and
verify
functions.

默认使用scrypt算法。若要使用Argon2id,需提供自定义
hash
verify
函数。

Two-Factor Authentication

双因素认证

Setup

配置

typescript
import { twoFactor } from "better-auth/plugins";

// Server
plugins: [
  twoFactor({
    issuer: "Grove",  // Shown in authenticator apps
    totpOptions: { digits: 6, period: 30 },
    backupCodeOptions: { amount: 10, length: 10, storeBackupCodes: "encrypted" },
  }),
]

// Client
plugins: [
  twoFactorClient({
    onTwoFactorRedirect() {
      window.location.href = "/2fa";
    },
  }),
]
Run migrations after adding. The
twoFactorEnabled
flag only activates after successful TOTP verification.
typescript
import { twoFactor } from "better-auth/plugins";

// 服务端
plugins: [
  twoFactor({
    issuer: "Grove",  // 在认证器应用中显示
    totpOptions: { digits: 6, period: 30 },
    backupCodeOptions: { amount: 10, length: 10, storeBackupCodes: "encrypted" },
  }),
]

// 客户端
plugins: [
  twoFactorClient({
    onTwoFactorRedirect() {
      window.location.href = "/2fa";
    },
  }),
]
添加后请运行迁移。
twoFactorEnabled
标志仅在TOTP验证成功后才会激活。

Sign-In Flow with 2FA

带双因素认证的登录流程

  1. User signs in with credentials
  2. Response includes
    twoFactorRedirect: true
  3. Session cookie removed temporarily
  4. Two-factor cookie set (10-minute expiration)
  5. User verifies via TOTP/OTP/backup code
  6. Session cookie restored
  1. 用户使用凭证登录
  2. 响应包含
    twoFactorRedirect: true
  3. 临时移除会话Cookie
  4. 设置双因素认证Cookie(10分钟过期)
  5. 用户通过TOTP/OTP/备用码完成验证
  6. 恢复会话Cookie

Trusted Devices

可信设备

Skip 2FA on subsequent sign-ins:
typescript
twoFactor({
  trustDeviceMaxAge: 30 * 24 * 60 * 60,  // 30 days
})

// During verification:
await authClient.twoFactor.verifyTotp({ code, trustDevice: true });
后续登录可跳过双因素认证:
typescript
twoFactor({
  trustDeviceMaxAge: 30 * 24 * 60 * 60,  // 30天
})

// 验证时:
await authClient.twoFactor.verifyTotp({ code, trustDevice: true });

OTP (Email/SMS)

OTP(邮箱/短信)

typescript
twoFactor({
  otpOptions: {
    sendOTP: async ({ user, otp }) => {
      await sendEmail({ to: user.email, subject: "Your code", text: `Code: ${otp}` });
    },
    period: 5,            // Minutes
    digits: 6,
    allowedAttempts: 5,
    storeOTP: "encrypted",
  },
})
typescript
twoFactor({
  otpOptions: {
    sendOTP: async ({ user, otp }) => {
      await sendEmail({ to: user.email, subject: "你的验证码", text: `验证码:${otp}` });
    },
    period: 5,            // 有效期(分钟)
    digits: 6,
    allowedAttempts: 5,
    storeOTP: "encrypted",
  },
})

Built-in 2FA Protections

内置双因素认证防护

  • Rate limiting: 3 requests per 10 seconds on 2FA endpoints
  • OTP attempt limiting: configurable max attempts
  • Constant-time comparison prevents timing attacks
  • TOTP secrets encrypted with symmetric encryption
  • Backup codes encrypted by default
  • Limitation: 2FA requires credential accounts — social-only accounts can't enable it

  • 速率限制:双因素认证端点10秒内最多3次请求
  • OTP尝试次数限制:可配置最大尝试次数
  • 固定时间比较防止时序攻击
  • TOTP密钥使用对称加密存储
  • 备用码默认加密存储
  • 限制: 双因素认证仅适用于凭证登录账户——纯社交登录账户无法启用

Organizations Plugin

组织插件

Multi-tenant organization support:
typescript
import { organization } from "better-auth/plugins";

plugins: [
  organization({
    // Limit who can create orgs
    allowUserToCreateOrganization: async (user) => {
      return user.emailVerified;
    },
  }),
]
多租户组织支持:
typescript
import { organization } from "better-auth/plugins";

plugins: [
  organization({
    // 限制可创建组织的用户
    allowUserToCreateOrganization: async (user) => {
      return user.emailVerified;
    },
  }),
]

Key Concepts

核心概念

  • Active organization stored in session — scopes API calls after
    setActive()
  • Default roles: owner (full), admin (management), member (basic)
  • Dynamic access control for custom runtime permissions
  • Teams group members within organizations
  • Invitations expire after 48 hours (configurable), email-specific
  • Safety: Last owner cannot be removed or leave

  • 活跃组织存储在会话中——调用
    setActive()
    后API请求会自动按组织范围过滤
  • 默认角色: owner(完全权限)、admin(管理权限)、member(基础权限)
  • 动态访问控制支持自定义运行时权限
  • 团队用于在组织内分组成员
  • 邀请链接48小时后过期(可配置),且与邮箱绑定
  • 安全机制: 最后一位owner无法被移除或退出组织

Plugins Reference

插件参考

typescript
import { twoFactor, organization } from "better-auth/plugins";
PluginPurposeScoped Package?
twoFactor
TOTP/OTP/backup codesNo
organization
Teams & multi-tenantNo
passkey
WebAuthn
@better-auth/passkey
magicLink
Passwordless emailNo
emailOtp
Email-based OTPNo
username
Username authNo
phoneNumber
Phone authNo
admin
User managementNo
apiKey
API key authNo
bearer
Bearer token authNo
jwt
JWT tokensNo
multiSession
Multiple sessionsNo
sso
SAML/OIDC enterprise
@better-auth/sso
oauthProvider
Be an OAuth providerNo
oidcProvider
Be an OIDC providerNo
openAPI
API documentationNo
genericOAuth
Custom OAuth providerNo
Client plugins go in
createAuthClient({ plugins: [...] })
.
Always run migrations after adding plugins.

typescript
import { twoFactor, organization } from "better-auth/plugins";
插件用途是否为独立包?
twoFactor
TOTP/OTP/备用码
organization
团队与多租户管理
passkey
WebAuthn密钥登录
@better-auth/passkey
magicLink
无密码邮箱登录
emailOtp
邮箱验证码登录
username
用户名登录
phoneNumber
手机号登录
admin
用户管理
apiKey
API密钥认证
bearer
Bearer令牌认证
jwt
JWT令牌
multiSession
多会话管理
sso
SAML/OIDC企业单点登录
@better-auth/sso
oauthProvider
作为OAuth提供商
oidcProvider
作为OIDC提供商
openAPI
API文档生成
genericOAuth
自定义OAuth提供商
客户端插件需配置在
createAuthClient({ plugins: [...] })
中。
添加插件后请务必运行迁移。

Client Setup

客户端设置

Import by framework:
FrameworkImport
React/Next.js
better-auth/react
Svelte/SvelteKit
better-auth/svelte
Vue/Nuxt
better-auth/vue
Solid
better-auth/solid
Vanilla JS
better-auth/client
Key methods:
signUp.email()
,
signIn.email()
,
signIn.social()
,
signOut()
,
useSession()
,
getSession()
,
revokeSession()
,
revokeSessions()
.
按框架导入:
框架导入路径
React/Next.js
better-auth/react
Svelte/SvelteKit
better-auth/svelte
Vue/Nuxt
better-auth/vue
Solid
better-auth/solid
原生JS
better-auth/client
核心方法:
signUp.email()
signIn.email()
signIn.social()
signOut()
useSession()
getSession()
revokeSession()
revokeSessions()

Type Safety

类型安全

typescript
// Infer types from server config
type Session = typeof auth.$Infer.Session;
type User = typeof auth.$Infer.Session.user;

// For separate client/server projects
createAuthClient<typeof auth>();

typescript
// 从服务端配置推断类型
type Session = typeof auth.$Infer.Session;
type User = typeof auth.$Infer.Session.user;

// 适用于客户端与服务端分离的项目
createAuthClient<typeof auth>();

Hooks

钩子

Endpoint Hooks

端点钩子

typescript
hooks: {
  before: [
    {
      matcher: (ctx) => ctx.path === "/sign-in/email",
      handler: createAuthMiddleware(async (ctx) => {
        // Access: ctx.path, ctx.context.session, ctx.context.secret
        // Return modified context or void
      }),
    },
  ],
  after: [
    {
      matcher: (ctx) => true,
      handler: createAuthMiddleware(async (ctx) => {
        // Access: ctx.context.returned (response data)
      }),
    },
  ],
}
typescript
hooks: {
  before: [
    {
      matcher: (ctx) => ctx.path === "/sign-in/email",
      handler: createAuthMiddleware(async (ctx) => {
        // 可访问:ctx.path、ctx.context.session、ctx.context.secret
        // 返回修改后的上下文或void
      }),
    },
  ],
  after: [
    {
      matcher: (ctx) => true,
      handler: createAuthMiddleware(async (ctx) => {
        // 可访问:ctx.context.returned(响应数据)
      }),
    },
  ],
}

Database Hooks

数据库钩子

typescript
databaseHooks: {
  user: {
    create: {
      before: async ({ data }) => { /* add defaults, return false to block */ },
      after: async ({ data }) => { /* audit log, send welcome email */ },
    },
  },
  session: {
    create: {
      after: async ({ data, ctx }) => {
        await auditLog("session.created", {
          userId: data.userId,
          ip: ctx?.request?.headers.get("x-forwarded-for"),
        });
      },
    },
  },
}
Hook context (
ctx.context
):
session
,
secret
,
authCookies
,
password.hash()
/
verify()
,
adapter
,
internalAdapter
,
generateId()
,
tables
,
baseURL
.

typescript
databaseHooks: {
  user: {
    create: {
      before: async ({ data }) => { /* 添加默认值,返回false可阻止创建 */ },
      after: async ({ data }) => { /* 审计日志、发送欢迎邮件 */ },
    },
  },
  session: {
    create: {
      after: async ({ data, ctx }) => {
        await auditLog("session.created", {
          userId: data.userId,
          ip: ctx?.request?.headers.get("x-forwarded-for"),
        });
      },
    },
  },
}
钩子上下文(
ctx.context
):
session
secret
authCookies
password.hash()
/
verify()
adapter
internalAdapter
generateId()
tables
baseURL

Common Gotchas

常见陷阱

  1. Model vs table name — Config uses ORM model name, not DB table name
  2. Plugin schema — Re-run CLI after adding plugins (always!)
  3. Secondary storage — Sessions go there by default, not DB
  4. Cookie cache — Custom session fields NOT cached, always re-fetched
  5. Stateless mode — No DB = session in cookie only, logout on cache expiry
  6. Change email flow — Sends to current email first, then new email
  7. 2FA + social — 2FA only works on credential accounts, not social-only
  8. Last owner — Cannot be removed from or leave an organization
  9. Rate limit memory — Memory storage resets on restart, bad for serverless

  1. 模型名 vs 表名 —— 配置中使用ORM模型名,而非数据库表名
  2. 插件Schema —— 添加插件后请重新运行CLI(务必执行!)
  3. 二级存储 —— 会话默认存储在此处,而非数据库
  4. Cookie缓存 —— 自定义会话字段不会被缓存,始终从存储中重新获取
  5. 无状态模式 —— 无数据库时会话仅存储在Cookie中,缓存过期即登出
  6. 修改邮箱流程 —— 先发送验证邮件到当前邮箱,再发送到新邮箱
  7. 双因素认证 + 社交登录 —— 双因素认证仅适用于凭证登录账户,纯社交登录账户无法启用
  8. 最后一位Owner —— 无法被移除或退出组织
  9. 内存速率限制 —— 内存存储会在重启后重置,不适用于无服务器环境

Production Security Checklist

生产环境安全检查清单

  • BETTER_AUTH_SECRET
    set (32+ chars, high entropy)
  • BETTER_AUTH_URL
    uses HTTPS
  • trustedOrigins
    configured for all valid origins
  • Rate limiting enabled with appropriate per-endpoint limits
  • Rate limit storage set to
    "secondary-storage"
    or
    "database"
    (not memory)
  • CSRF protection enabled (
    disableCSRFCheck: false
    )
  • Secure cookies enabled (automatic with HTTPS)
  • account.encryptOAuthTokens: true
    if storing tokens
  • Background tasks configured for serverless (capture
    ExecutionContext
    from fetch handler)
  • Audit logging via
    databaseHooks
    or
    hooks
  • IP tracking headers configured if behind proxy
  • Email verification enabled
  • Password reset implemented
  • 2FA available for sensitive apps
  • Session expiry and refresh intervals reviewed
  • Cookie cache strategy chosen (
    jwe
    for sensitive session data)
  • account.accountLinking
    reviewed

  • 已设置
    BETTER_AUTH_SECRET
    (32+字符,高熵值)
  • BETTER_AUTH_URL
    使用HTTPS
  • 已为所有有效来源配置
    trustedOrigins
  • 已启用速率限制,并配置了合适的按端点限制规则
  • 速率限制存储设置为
    "secondary-storage"
    "database"
    (而非memory)
  • 已启用CSRF防护(
    disableCSRFCheck: false
  • 已启用安全Cookie(HTTPS环境下自动启用)
  • 若存储OAuth令牌,已设置
    account.encryptOAuthTokens: true
  • 已为无服务器环境配置后台任务(从fetch处理函数中捕获
    ExecutionContext
  • 通过
    databaseHooks
    hooks
    实现了审计日志
  • 若部署在代理后,已配置IP跟踪头
  • 已启用邮箱验证
  • 已实现密码重置功能
  • 敏感应用已提供双因素认证选项
  • 已审查会话过期和刷新间隔
  • 已选择合适的Cookie缓存策略(敏感会话数据推荐使用
    jwe
  • 已审查
    account.accountLinking
    配置

Complete Production Config Example

完整生产环境配置示例

typescript
import { betterAuth } from "better-auth";
import { twoFactor, organization } from "better-auth/plugins";

// Factory pattern — ctx comes from the Worker fetch handler
export function createAuth(env: Env, ctx: ExecutionContext) {
  return betterAuth({
    appName: "Grove",
    secret: env.BETTER_AUTH_SECRET,
    baseURL: "https://auth-api.grove.place",
    trustedOrigins: [
      "https://heartwood.grove.place",
      "https://*.grove.place",
    ],

    database: d1Adapter(env),
    secondaryStorage: kvAdapter(env),

    // Rate limiting (replaces custom threshold SDK for auth)
    rateLimit: {
      enabled: true,
      storage: "secondary-storage",
      customRules: {
        "/api/auth/sign-in/email": { window: 60, max: 5 },
        "/api/auth/sign-up/email": { window: 60, max: 3 },
        "/api/auth/change-password": { window: 60, max: 3 },
      },
    },

    // Sessions
    session: {
      expiresIn: 60 * 60 * 24 * 7,    // 7 days
      updateAge: 60 * 60 * 24,         // 24 hours
      freshAge: 60 * 60,               // 1 hour for sensitive actions
      cookieCache: {
        enabled: true,
        maxAge: 300,
        strategy: "jwe",
      },
    },

    // OAuth
    account: {
      encryptOAuthTokens: true,
      storeStateStrategy: "cookie",
    },

    // Security
    advanced: {
      useSecureCookies: true,
      crossSubDomainCookies: {
        enabled: true,
        domain: ".grove.place",
      },
      ipAddress: {
        ipAddressHeaders: ["x-forwarded-for"],
        ipv6Subnet: 64,
      },
      backgroundTasks: {
        handler: (promise) => ctx.waitUntil(promise),  // ctx captured from fetch handler
      },
    },

    // Plugins
    plugins: [
      twoFactor({
        issuer: "Grove",
        backupCodeOptions: { storeBackupCodes: "encrypted" },
      }),
      organization(),
    ],

    // Audit hooks
    databaseHooks: {
      session: {
        create: {
          after: async ({ data, ctx: hookCtx }) => {
            console.log(`[audit] session created: user=${data.userId}`);
          },
        },
      },
    },
  });
}

typescript
import { betterAuth } from "better-auth";
import { twoFactor, organization } from "better-auth/plugins";

// 工厂模式 —— ctx来自Worker的fetch处理函数
export function createAuth(env: Env, ctx: ExecutionContext) {
  return betterAuth({
    appName: "Grove",
    secret: env.BETTER_AUTH_SECRET,
    baseURL: "https://auth-api.grove.place",
    trustedOrigins: [
      "https://heartwood.grove.place",
      "https://*.grove.place",
    ],

    database: d1Adapter(env),
    secondaryStorage: kvAdapter(env),

    // 速率限制(替代认证端点的自定义阈值SDK)
    rateLimit: {
      enabled: true,
      storage: "secondary-storage",
      customRules: {
        "/api/auth/sign-in/email": { window: 60, max: 5 },
        "/api/auth/sign-up/email": { window: 60, max: 3 },
        "/api/auth/change-password": { window: 60, max: 3 },
      },
    },

    // 会话配置
    session: {
      expiresIn: 60 * 60 * 24 * 7,    // 7天
      updateAge: 60 * 60 * 24,         // 24小时
      freshAge: 60 * 60,               // 敏感操作要求会话有效期不超过1小时
      cookieCache: {
        enabled: true,
        maxAge: 300,
        strategy: "jwe",
      },
    },

    // OAuth配置
    account: {
      encryptOAuthTokens: true,
      storeStateStrategy: "cookie",
    },

    // 安全配置
    advanced: {
      useSecureCookies: true,
      crossSubDomainCookies: {
        enabled: true,
        domain: ".grove.place",
      },
      ipAddress: {
        ipAddressHeaders: ["x-forwarded-for"],
        ipv6Subnet: 64,
      },
      backgroundTasks: {
        handler: (promise) => ctx.waitUntil(promise),  // ctx从fetch处理函数中捕获
      },
    },

    // 插件
    plugins: [
      twoFactor({
        issuer: "Grove",
        backupCodeOptions: { storeBackupCodes: "encrypted" },
      }),
      organization(),
    ],

    // 审计钩子
    databaseHooks: {
      session: {
        create: {
          after: async ({ data, ctx: hookCtx }) => {
            console.log(`[audit] session created: user=${data.userId}`);
          },
        },
      },
    },
  });
}

Resources

资源