better-auth-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBetter 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: (Grove-specific integration), (auth architecture), (deep security)
heartwood-authspider-weaveturtle-harden- 配置或修改Better Auth服务端/客户端设置
- 审核认证安全性(可搭配、
raccoon-audit使用)turtle-harden - 添加或配置速率限制
- 设置会话管理、Cookie缓存或二级存储
- 添加插件(双因素认证、组织管理、密钥登录等)
- 排查Heartwood或任何Better Auth部署中的认证问题
- 生产环境部署前审查安全状态
搭配技能: (Grove专属集成方案)、(认证架构)、(深度安全加固)
heartwood-authspider-weaveturtle-hardenGrove Context: Heartwood
Grove 专属上下文:Heartwood
Heartwood is Grove's auth service, powered by Better Auth on Cloudflare Workers.
| Component | Detail |
|---|---|
| Frontend | |
| API | |
| Database | Cloudflare D1 (SQLite) |
| Session cache | Cloudflare KV ( |
| Providers | Google OAuth, Magic Links, Passkeys |
| Cookie domain | |
Everything in this skill applies directly to Heartwood. The skill covers Grove-specific integration patterns (client setup, route protection, error codes). This skill covers the framework itself.
heartwood-authHeartwood是Grove的认证服务,基于Cloudflare Workers上的Better Auth构建。
| 组件 | 详情 |
|---|---|
| 前端 | |
| API | |
| 数据库 | Cloudflare D1(SQLite) |
| 会话缓存 | Cloudflare KV( |
| 登录提供商 | Google OAuth、魔法链接、密钥登录 |
| Cookie域名 | |
本技能中的所有内容均直接适用于Heartwood。技能涵盖Grove专属的集成模式(客户端设置、路由保护、错误码),而本技能聚焦框架本身。
heartwood-authQuick Reference
快速参考
Environment Variables
环境变量
| Variable | Purpose |
|---|---|
| Encryption secret (min 32 chars). Generate: |
| Base URL (e.g., |
| Comma-separated trusted origins |
Only define / in config if env vars are NOT set.
baseURLsecret| 变量 | 用途 |
|---|---|
| 加密密钥(至少32字符)。生成方式: |
| 基础URL(例如: |
| 逗号分隔的可信源列表 |
仅当未设置环境变量时,才在配置中定义/。
baseURLsecretFile Location
文件位置
CLI looks for in: , , , or under . Use for custom path.
auth.ts././lib./utils./src--configCLI会在以下目录查找:、、或下。使用指定自定义路径。
auth.ts././lib./utils./src--configCLI Commands
CLI命令
bash
npx @better-auth/cli@latest migrate # Apply schema (built-in adapter)
npx @better-auth/cli@latest generate # Generate schema for Prisma/DrizzleRe-run after adding/changing plugins.
bash
npx @better-auth/cli@latest migrate # 应用数据库 schema(内置适配器)
npx @better-auth/cli@latest generate # 为Prisma/Drizzle生成schema添加或修改插件后请重新运行上述命令。
Core Configuration
核心配置
| Option | Notes |
|---|---|
| Display name (used in 2FA issuer, emails) |
| Only if |
| Default |
| Only if |
| Required. Connection or adapter instance |
| Redis/KV for sessions & rate limits |
| |
| |
| Array of plugins |
| CSRF whitelist (baseURL auto-trusted) |
| 选项 | 说明 |
|---|---|
| 显示名称(用于双因素认证发行方、邮件内容) |
| 仅当未设置 |
| 默认值 |
| 仅当未设置 |
| 必填项。数据库连接或适配器实例 |
| 用于会话和速率限制的Redis/KV存储 |
| 设置 |
| 配置示例: |
| 插件数组 |
| CSRF白名单(baseURL会自动加入可信列表) |
Database
数据库
Direct connections: Pass , pool, , or instance.
pg.Poolmysql2better-sqlite3bun:sqliteORM adapters: Import from , , .
better-auth/adapters/drizzlebetter-auth/adapters/prismabetter-auth/adapters/mongodbCritical gotcha: Better Auth uses adapter model names, NOT underlying table names. If Prisma model is mapping to table , use (Prisma reference), not .
UserusersmodelName: "user""users"直接连接: 传入、连接池、或实例。
pg.Poolmysql2better-sqlite3bun:sqliteORM适配器: 从、、导入。
better-auth/adapters/drizzlebetter-auth/adapters/prismabetter-auth/adapters/mongodb关键注意事项: Better Auth使用适配器模型名称,而非底层数据库表名。如果Prisma模型为,对应数据库表为,则需使用(参考Prisma命名规则),而非。
UserusersmodelName: "user""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
}| Storage | Behavior |
|---|---|
| Fast, resets on restart. Not recommended for serverless. |
| Persistent, adds DB load |
| Uses configured KV/Redis. Default when available. |
For Heartwood: Use backed by Cloudflare KV.
"secondary-storage"typescript
rateLimit: {
storage: "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: 3 requests per 10 seconds/change-email
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:10秒内最多3次请求/change-email
可针对特定路径覆盖默认规则:
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
存储优先级
- If defined → sessions go there (not DB)
secondaryStorage - Set to also persist to DB
session.storeSessionInDatabase: true - No database + → fully stateless mode
cookieCache
- 若配置了→ 会话存储在此处(而非数据库)
secondaryStorage - 设置可同时将会话持久化到数据库
session.storeSessionInDatabase: true - 无数据库 + 启用→ 完全无状态模式
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)
}freshAgetypescript
session: {
expiresIn: 60 * 60 * 24 * 7, // 7天(默认值)
updateAge: 60 * 60 * 24, // 每24小时刷新一次(默认值)
freshAge: 60 * 60 * 24, // 敏感操作要求会话有效期不超过24小时(默认值)
}freshAgeCookie 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
},
}| Strategy | Description |
|---|---|
| Base64url + HMAC. Smallest size. Default. |
| Standard HS256 JWT. Readable but signed. |
| 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, // 修改版本号可使所有会话失效
},
}| 策略 | 描述 |
|---|---|
| Base64url + HMAC加密。体积最小,为默认选项。 |
| 标准HS256 JWT格式。内容可读但已签名。 |
| A256CBC-HS512加密。安全性最高。 |
注意事项: 自定义会话字段不会被缓存——始终会从存储中重新获取。
Security Configuration
安全配置
Secret Management
密钥管理
Better Auth looks for secrets in order:
- in config
options.secret - env var
BETTER_AUTH_SECRET - env var
AUTH_SECRET
Requirements:
- Rejects default/placeholder secrets in production
- Warns if shorter than 32 characters
- Warns if entropy below 120 bits
Better Auth按以下顺序查找密钥:
- 配置中的
options.secret - 环境变量
BETTER_AUTH_SECRET - 环境变量
AUTH_SECRET
要求:
- 生产环境中拒绝使用默认/占位符密钥
- 若密钥长度不足32字符会发出警告
- 若密钥熵值低于120位会发出警告
CSRF Protection
CSRF防护
Multi-layered by default:
- Origin header validation — /
Originmust match trusted originsReferer - Fetch metadata — Uses ,
Sec-Fetch-Site,Sec-Fetch-ModeheadersSec-Fetch-Dest - First-login protection — Validates origin even without cookies
typescript
advanced: {
disableCSRFCheck: false, // KEEP THIS FALSE
}默认启用多层防护:
- Origin头验证 —— /
Origin必须匹配可信源Referer - Fetch元数据 —— 使用、
Sec-Fetch-Site、Sec-Fetch-Mode头Sec-Fetch-Dest - 首次登录防护 —— 即使无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: , , , , , and more. Invalid URLs get 403.
callbackURLredirectToerrorCallbackURLnewUserCallbackURLorigintypescript
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`];
}验证参数包括:、、、、等。无效URL会返回403。
callbackURLredirectToerrorCallbackURLnewUserCallbackURLoriginCookie Security
Cookie安全
Defaults are secure:
- when baseURL uses HTTPS or in production
secure: true - (CSRF prevention while allowing navigation)
sameSite: "lax" - (no JavaScript access)
httpOnly: true - prefix when secure is enabled
__Secure-
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 - (防止CSRF同时允许正常导航)
sameSite: "lax" - (禁止JavaScript访问)
httpOnly: true - 启用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 callback receives a promise that must outlive the response — on serverless platforms, you need the platform's to keep it alive.
handlerwaitUntilCloudflare Workers: Capture from the fetch handler and close over it:
ExecutionContexttypescript
// 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 export from :
waitUntil@vercel/functionstypescript
import { waitUntil } from "@vercel/functions";
advanced: {
backgroundTasks: {
handler: (promise) => waitUntil(promise),
},
}Ensures email sending doesn't leak information about whether a user exists.
敏感操作应在固定时间内完成。回调会接收一个Promise,该Promise必须在响应完成后继续执行——在无服务器平台上,需使用平台的方法保持其存活。
handlerwaitUntilCloudflare Workers: 从fetch处理函数中捕获并在闭包中使用:
ExecutionContexttypescript
// 在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/functionswaitUntiltypescript
import { waitUntil } from "@vercel/functions";
advanced: {
backgroundTasks: {
handler: (promise) => waitUntil(promise),
},
}确保邮件发送操作不会泄露用户是否存在的信息。
Account Enumeration Prevention
账户枚举防护
Built-in protections:
- Consistent response messages — Password reset always returns generic message
- Dummy operations — When user isn't found, still performs token generation + DB lookups
- Background email sending — Async to prevent timing differences
内置防护机制:
- 统一响应信息 —— 密码重置请求始终返回通用提示
- 虚拟操作 —— 当用户不存在时,仍会执行令牌生成和数据库查询操作
- 后台发送邮件 —— 异步执行以避免时序差异
OAuth / Social Provider Security
OAuth / 社交登录提供商安全
PKCE (Automatic)
PKCE(自动启用)
Better Auth automatically uses PKCE for all OAuth flows:
- Generates 128-character random
code_verifier - Creates using S256 (SHA-256)
code_challenge - Validates code exchange with original verifier
Better Auth自动为所有OAuth流程使用PKCE:
- 生成128字符的随机
code_verifier - 使用S256(SHA-256)生成
code_challenge - 使用原始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 and functions.
hashverify默认使用scrypt算法。若要使用Argon2id,需提供自定义和函数。
hashverifyTwo-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 flag only activates after successful TOTP verification.
twoFactorEnabledtypescript
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";
},
}),
]添加后请运行迁移。 标志仅在TOTP验证成功后才会激活。
twoFactorEnabledSign-In Flow with 2FA
带双因素认证的登录流程
- User signs in with credentials
- Response includes
twoFactorRedirect: true - Session cookie removed temporarily
- Two-factor cookie set (10-minute expiration)
- User verifies via TOTP/OTP/backup code
- Session cookie restored
- 用户使用凭证登录
- 响应包含
twoFactorRedirect: true - 临时移除会话Cookie
- 设置双因素认证Cookie(10分钟过期)
- 用户通过TOTP/OTP/备用码完成验证
- 恢复会话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
- 活跃组织存储在会话中——调用后API请求会自动按组织范围过滤
setActive() - 默认角色: owner(完全权限)、admin(管理权限)、member(基础权限)
- 动态访问控制支持自定义运行时权限
- 团队用于在组织内分组成员
- 邀请链接48小时后过期(可配置),且与邮箱绑定
- 安全机制: 最后一位owner无法被移除或退出组织
Plugins Reference
插件参考
typescript
import { twoFactor, organization } from "better-auth/plugins";| Plugin | Purpose | Scoped Package? |
|---|---|---|
| TOTP/OTP/backup codes | No |
| Teams & multi-tenant | No |
| WebAuthn | |
| Passwordless email | No |
| Email-based OTP | No |
| Username auth | No |
| Phone auth | No |
| User management | No |
| API key auth | No |
| Bearer token auth | No |
| JWT tokens | No |
| Multiple sessions | No |
| SAML/OIDC enterprise | |
| Be an OAuth provider | No |
| Be an OIDC provider | No |
| API documentation | No |
| Custom OAuth provider | No |
Client plugins go in .
createAuthClient({ plugins: [...] })Always run migrations after adding plugins.
typescript
import { twoFactor, organization } from "better-auth/plugins";| 插件 | 用途 | 是否为独立包? |
|---|---|---|
| TOTP/OTP/备用码 | 否 |
| 团队与多租户管理 | 否 |
| WebAuthn密钥登录 | |
| 无密码邮箱登录 | 否 |
| 邮箱验证码登录 | 否 |
| 用户名登录 | 否 |
| 手机号登录 | 否 |
| 用户管理 | 否 |
| API密钥认证 | 否 |
| Bearer令牌认证 | 否 |
| JWT令牌 | 否 |
| 多会话管理 | 否 |
| SAML/OIDC企业单点登录 | |
| 作为OAuth提供商 | 否 |
| 作为OIDC提供商 | 否 |
| API文档生成 | 否 |
| 自定义OAuth提供商 | 否 |
客户端插件需配置在中。
createAuthClient({ plugins: [...] })添加插件后请务必运行迁移。
Client Setup
客户端设置
Import by framework:
| Framework | Import |
|---|---|
| React/Next.js | |
| Svelte/SvelteKit | |
| Vue/Nuxt | |
| Solid | |
| Vanilla JS | |
Key methods: , , , , , , , .
signUp.email()signIn.email()signIn.social()signOut()useSession()getSession()revokeSession()revokeSessions()按框架导入:
| 框架 | 导入路径 |
|---|---|
| React/Next.js | |
| Svelte/SvelteKit | |
| Vue/Nuxt | |
| Solid | |
| 原生JS | |
核心方法:、、、、、、、。
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.contextsessionsecretauthCookiespassword.hash()verify()adapterinternalAdaptergenerateId()tablesbaseURLtypescript
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.contextsessionsecretauthCookiespassword.hash()verify()adapterinternalAdaptergenerateId()tablesbaseURLCommon Gotchas
常见陷阱
- Model vs table name — Config uses ORM model name, not DB table name
- Plugin schema — Re-run CLI after adding plugins (always!)
- Secondary storage — Sessions go there by default, not DB
- Cookie cache — Custom session fields NOT cached, always re-fetched
- Stateless mode — No DB = session in cookie only, logout on cache expiry
- Change email flow — Sends to current email first, then new email
- 2FA + social — 2FA only works on credential accounts, not social-only
- Last owner — Cannot be removed from or leave an organization
- Rate limit memory — Memory storage resets on restart, bad for serverless
- 模型名 vs 表名 —— 配置中使用ORM模型名,而非数据库表名
- 插件Schema —— 添加插件后请重新运行CLI(务必执行!)
- 二级存储 —— 会话默认存储在此处,而非数据库
- Cookie缓存 —— 自定义会话字段不会被缓存,始终从存储中重新获取
- 无状态模式 —— 无数据库时会话仅存储在Cookie中,缓存过期即登出
- 修改邮箱流程 —— 先发送验证邮件到当前邮箱,再发送到新邮箱
- 双因素认证 + 社交登录 —— 双因素认证仅适用于凭证登录账户,纯社交登录账户无法启用
- 最后一位Owner —— 无法被移除或退出组织
- 内存速率限制 —— 内存存储会在重启后重置,不适用于无服务器环境
Production Security Checklist
生产环境安全检查清单
- set (32+ chars, high entropy)
BETTER_AUTH_SECRET - uses HTTPS
BETTER_AUTH_URL - configured for all valid origins
trustedOrigins - Rate limiting enabled with appropriate per-endpoint limits
- Rate limit storage set to or
"secondary-storage"(not memory)"database" - CSRF protection enabled ()
disableCSRFCheck: false - Secure cookies enabled (automatic with HTTPS)
- if storing tokens
account.encryptOAuthTokens: true - Background tasks configured for serverless (capture from fetch handler)
ExecutionContext - Audit logging via or
databaseHookshooks - 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 (for sensitive session data)
jwe - reviewed
account.accountLinking
- 已设置(32+字符,高熵值)
BETTER_AUTH_SECRET - 使用HTTPS
BETTER_AUTH_URL - 已为所有有效来源配置
trustedOrigins - 已启用速率限制,并配置了合适的按端点限制规则
- 速率限制存储设置为或
"secondary-storage"(而非memory)"database" - 已启用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}`);
},
},
},
},
});
}