数据库

学习如何使用 Better Auth 连接数据库。

适配器

Better Auth 连接数据库以存储数据。数据库将用于存储用户、会话等数据。插件也可以定义自己的数据库表以存储数据。

你可以通过在数据库选项中传入支持的数据库实例,将数据库连接传递给 Better Auth。你可以在 其他关系型数据库 文档中了解更多支持的数据库适配器。

Better Auth 同样支持无数据库的使用。更多详情,请参见 无状态会话管理

命令行工具(CLI)

Better Auth 配备了 CLI 工具,用于管理数据库迁移和生成架构。

运行迁移

CLI 会检查你的数据库,并提示你添加缺失的表或用新列更新现有表。这仅支持内置的 Kysely 适配器。对于其他适配器,可以使用 generate 命令生成架构,并通过你的 ORM 处理迁移。

npx auth@latest migrate

针对 PostgreSQL 用户:migrate 命令支持非默认 schema。它会自动检测你的 search_path 配置,并在正确的 schema 中创建表。详细请参见 PostgreSQL 适配器

生成架构

Better Auth 还提供了 generate 命令,用于生成 Better Auth 所需的架构。若你使用的是 Prisma 或 Drizzle 等数据库适配器,此命令会为你的 ORM 生成正确的架构。如果使用内置的 Kysely 适配器,它将生成一个 SQL 文件,可直接运行于你的数据库。

npx auth@latest generate

更多 CLI 信息,请参阅 CLI 文档。

如果你更喜欢手动添加表也可以。Better Auth 所需的核心架构如下所述,插件文档中可以找到插件所需的额外架构。

编程式迁移

在无法使用 CLI 的环境中(例如 Cloudflare Workers,无服务器函数),你可以使用 better-auth/db/migration 中的 getMigrations 编程运行迁移。

import { getMigrations } from "better-auth/db/migration";
import { auth } from "./auth";

const { toBeCreated, toBeAdded, runMigrations } = await getMigrations(auth.options);

await runMigrations();

getMigrations 仅支持内置的 Kysely 适配器(SQLite/D1、PostgreSQL、MySQL、MSSQL)。不支持 Prisma 或 Drizzle ORM 适配器 —— 对于它们,请使用 CLI 迁移。

二级存储

Better Auth 的二级存储允许你使用键值存储来管理会话数据、限速计数器等。当你希望将大量记录存储外包到高性能存储甚至内存时,这很有用。

实现

要使用二级存储,实现 SecondaryStorage 接口:

interface SecondaryStorage {
  get: (key: string) => Promise<unknown>;
  set: (key: string, value: string, ttl?: number) => Promise<void>;
  delete: (key: string) => Promise<void>;
}

然后,将你的实现传递给 betterAuth 函数:

auth.ts
import { betterAuth } from "better-auth";

betterAuth({
  // ... 其他选项
  secondaryStorage: {
    // 你的实现
  },
});

Redis 存储

Better Auth 提供官方的 Redis 存储包,基于 ioredis

npm install @better-auth/redis-storage ioredis
# 或者
pnpm add @better-auth/redis-storage ioredis

使用示例:

import { betterAuth } from "better-auth";
import { Redis } from "ioredis";
import { redisStorage } from "@better-auth/redis-storage";

const redis = new Redis({
	host: "localhost",
	port: 6379,
});

export const auth = betterAuth({
	// ... 其他选项
	secondaryStorage: redisStorage({
		client: redis,
		keyPrefix: "better-auth:", // 可选,默认为 "better-auth:"
	}),
});

Redis 存储支持 ioredis 的所有连接模式,包括单机、集群和哨兵配置。

手动实现示例:

如果你想自己实现 Redis 二级存储,下面是一个基础示例:

auth.ts
import { createClient } from "redis";
import { betterAuth } from "better-auth";

const redis = createClient();
await redis.connect();

export const auth = betterAuth({
	// ... 其他选项
	secondaryStorage: {
		get: async (key) => {
			return await redis.get(key);
		},
		set: async (key, value, ttl) => {
			if (ttl) await redis.set(key, value, { EX: ttl });
			else await redis.set(key, value);
		},
		delete: async (key) => {
			await redis.del(key);
		}
	}
});

此实现允许 Better Auth 使用 Redis 存储会话数据和限速计数器。你还可以为键名添加前缀。

核心架构

Better Auth 需要数据库中存在以下表。下面是 typescript 格式的类型定义。你可以在数据库中使用对应类型。

用户表

表名:user

Table
字段
类型
描述
id
string
pk
每个用户的唯一标识
name
string
用户选定的显示名称
email
string
用户用于通信和登录的邮箱地址
emailVerified
boolean
用户邮箱是否已验证
image
string
?
用户头像地址
createdAt
Date
用户账户创建的时间戳
updatedAt
Date
用户信息最后更新的时间戳

会话表

表名:session

Table
字段
类型
描述
id
string
pk
每个会话的唯一标识
userId
string
fk
用户 ID
token
string
唯一会话令牌
expiresAt
Date
会话过期时间
ipAddress
string
?
设备 IP 地址
userAgent
string
?
设备的用户代理信息
createdAt
Date
会话创建时间戳
updatedAt
Date
会话更新时间戳

账户表

表名:account

Table
字段
类型
描述
id
string
pk
每个账户的唯一标识
userId
string
fk
用户 ID
accountId
string
由 SSO 提供的账户 ID,或凭证账户等同于 userId
providerId
string
提供商 ID
accessToken
string
?
账户访问令牌,由提供商返回
refreshToken
string
?
账户刷新令牌,由提供商返回
accessTokenExpiresAt
Date
?
访问令牌过期时间
refreshTokenExpiresAt
Date
?
刷新令牌过期时间
scope
string
?
账户权限范围,由提供商返回
idToken
string
?
由提供商返回的 ID 令牌
password
string
?
账户密码,主要用于邮箱密码认证
createdAt
Date
账户创建时间戳
updatedAt
Date
账户更新时间戳

验证表

表名:verification

Table
字段
类型
描述
id
string
pk
每个验证请求的唯一标识
identifier
string
验证请求的标识符
value
string
待验证的值
expiresAt
Date
验证请求过期时间
createdAt
Date
验证请求创建时间戳
updatedAt
Date
验证请求更新时间戳

自定义表

Better Auth 允许自定义核心架构的表名和列名。你也可以通过向用户和会话表添加附加字段来扩展核心架构。

自定义表名

你可以通过在认证配置中使用 modelNamefields 属性来定制核心架构的表名和列名:

auth.ts
export const auth = betterAuth({
  user: {
    modelName: "users",
    fields: {
      name: "full_name",
      email: "email_address",
    },
  },
  session: {
    modelName: "user_sessions",
    fields: {
      userId: "user_id",
    },
  },
});

你代码中的类型推断依然使用原始字段名(例如,user.name,而非 user.full_name)。

对于插件自定义表名和列名,可以使用插件配置中的 schema 属性:

auth.ts
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    twoFactor({
      schema: {
        user: {
          fields: {
            twoFactorEnabled: "two_factor_enabled",
            secret: "two_factor_secret",
          },
        },
      },
    }),
  ],
});

扩展核心架构

Better Auth 提供类型安全方式扩展 usersession 架构。你可以在认证配置中添加自定义字段,CLI 会自动更新数据库架构。这些附加字段会被 useSessionsignUp.email 等接收用户或会话对象的函数正确推断。

要添加自定义字段,在认证配置的 usersession 对象中使用 additionalFields 属性。additionalFields 对象以字段名为键,值为 FieldAttributes 对象,包含:

  • type:字段数据类型,如 "string""number""boolean"
  • required:布尔值,表示字段是否必填。
  • defaultValue:字段的默认值(注意:只在 JavaScript 层生效,数据库层字段仍为可选)。
  • input:决定是否允许在创建新记录时输入该值(默认值为 true)。如果有如 role 这样的字段不应由用户注册时提供,可设置为 false

下面是扩展用户架构添加附加字段的示例:

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  user: {
    additionalFields: {
      role: {
        type: ["user", "admin"],
        required: false,
        defaultValue: "user",
        input: false, // 不允许用户设置角色
      },
      lang: {
        type: "string",
        required: false,
        defaultValue: "en",
      },
    },
  },
});

这样你就可以在应用逻辑中访问这些附加字段了。

// 注册时
const res = await auth.api.signUpEmail({
	body: {
		email: 'test@example.com',
		password: 'password',
		name: 'John Doe',
		lang: 'fr',
	},
});

// user 对象
res.user.role; // > "admin"
res.user.lang; // > "fr"

更多关于如何在客户端推断附加字段信息,请参见 TypeScript 文档。

若你使用社交/OAuth 提供商,可能想提供 mapProfileToUser 来将配置文件数据映射到用户对象,从而从提供商配置文件中填充附加字段。

映射 profile 到用户,示例:映射 firstNamelastName

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  socialProviders: {
    github: {
      clientId: "YOUR_GITHUB_CLIENT_ID",
      clientSecret: "YOUR_GITHUB_CLIENT_SECRET",
      mapProfileToUser: (profile) => {
        return {
          firstName: profile.name.split(" ")[0],
          lastName: profile.name.split(" ")[1],
        };
      },
    },
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
      mapProfileToUser: (profile) => {
        return {
          firstName: profile.given_name,
          lastName: profile.family_name,
        };
      },
    },
  },
});

ID 生成

默认情况下,Better Auth 会为用户、会话和其他实体生成唯一 ID。 你可以通过 advanced.database.generateId 选项自定义 ID 生成行为。

选项 1:让数据库生成 ID

generateId 设置为 false,允许数据库处理所有 ID 生成:(除了 generateId"serial" 及某些 uuid 的情况)

auth.ts
import { betterAuth } from "better-auth";
import { db } from "./db";

export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      generateId: false, // 如果想使用自增数字 ID,使用 "serial"
    },
  },
});

选项 2:自定义 ID 生成函数

使用函数生成 ID。函数返回 falseundefined 表示让数据库为特定模型生成 ID:

auth.ts
import { betterAuth } from "better-auth";
import { db } from "./db";

export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      generateId: (options) => {
        // 对特定模型让数据库自动生成
        if (options.model === "user" || options.model === "users") {
          return false; // 数据库生成 ID
        }
        // 其他表生成 UUID
        return crypto.randomUUID();
      },
    },
  },
});

重要generateId 函数返回 falseundefined 表示对应模型由数据库生成 ID。直接设置 generateId: false(非函数)会禁用 所有 表的 ID 生成。

选项 3:统一自定义 ID 生成器

为所有表生成相同类型的 ID:

auth.ts
import { betterAuth } from "better-auth";
import { db } from "./db";

export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      generateId: () => crypto.randomUUID(),
    },
  },
});

数字 ID

若你喜欢自增数字 ID,可将 advanced.database.generateId 设置为 "serial"。 此时,Better Auth 不会生成 ID,而是假设数据库自动生成数字 ID。

启用后,Better Auth CLI 会以数字类型(带有自增属性)生成或迁移你数据库的 id 字段。

auth.ts
import { betterAuth } from "better-auth";
import { db } from "./db";

export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      generateId: "serial",
    },
  },
});

Better Auth 依然会将数据库中 id 字段视为字符串类型,获取或插入数据时会自动转换为数字类型。

你从 Better Auth 获取的 id 可能是数字的字符串形式,这正常。传给 Better Auth 的 id 也应为字符串。

UUID

如果你偏好 UUID 类型的 id 字段,可以将 advanced.database.generateId 设为 "uuid"。 默认情况下,Better Auth 会为所有表生成 UUID,除了支持数据库自动生成 UUID 的 PostgreSQL 适配器。

启用该选项后,Better Auth CLI 会生成或迁移带 UUID 类型 id 字段的数据库架构。 如果数据库不支持 UUID 类型,则退化为普通字符串类型。

混合 ID 类型

若需跨表使用不同 ID 类型(例如用户使用整数 ID,其他表用 UUID 字符串),请使用 generateId 回调函数方式。

auth.ts
import { betterAuth } from "better-auth";
import { db } from "./db";

export const auth = betterAuth({
  database: db,
  user: {
    modelName: "users", // PostgreSQL:id 为 serial 主键
  },
  session: {
    modelName: "session", // PostgreSQL:id 为 text 主键
  },
  advanced: {
    database: {
      // 不要设 useNumberId — 该选项是全局的,影响所有表
      generateId: (options) => {
        if (options.model === "user" || options.model === "users") {
          return false; // 让 PostgreSQL serial 生成 ID
        }
        return crypto.randomUUID(); // session、account、verification 生成 UUID
      },
    },
  },
});

该配置支持:

  • 用户表使用数据库自动递增(serial、auto_increment 等)
  • 其他表(会话、账户、验证)生成 UUID
  • 兼容已有使用不同 ID 类型的架构

用例:当从其他认证提供商(如 Clerk)迁移时非常有用,你的用户 ID 可能为整数,而新表使用 UUID 字符串。

数据库钩子

数据库钩子允许你定义自定义逻辑,在 Better Auth 核心数据库操作生命周期中执行。你可为以下模型创建钩子:usersessionaccount

支持附加字段,但当前尚未支持这些字段的完整类型推断,后续会改进。

你可以定义两种类型的钩子:

1. 前置钩子(Before Hook)

  • 目的:在对应实体(用户、会话或账户)被创建、更新或删除之前调用。
  • 行为:返回 false 会中止操作,返回数据对象则会替代原始负载。

2. 后置钩子(After Hook)

  • 目的:对应实体创建或更新成功后调用。
  • 行为:可执行额外操作或修改。

示例用法

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          // 在创建前修改用户对象
          return {
            data: {
              // 注意返回 Better Auth 命名的字段,而非数据库原始字段名
              ...user,
              firstName: user.name.split(" ")[0],
              lastName: user.name.split(" ")[1],
            },
          };
        },
        after: async (user) => {
          // 可执行额外动作,如创建 Stripe 客户
        },
      },
      delete: {
        before: async (user, ctx) => {
          console.log(`用户 ${user.email} 正在被删除`);
          if (user.email.includes("admin")) {
            return false; // 中止删除
          }
          return true; // 允许删除
        },
        after: async (user) => {
          console.log(`用户 ${user.email} 已被删除`);
        },
      },
    },
    session: {
      delete: {
        before: async (session, ctx) => {
          console.log(`会话 ${session.token} 正在被删除`);
          if (session.userId === "admin-user-id") {
            return false; // 中止删除
          }
          return true; // 允许删除
        },
        after: async (session) => {
          console.log(`会话 ${session.token} 已被删除`);
        },
      },
    },
  },
});

抛出错误

如果想在数据库钩子中阻止继续进行,可以使用从 better-auth/api 导入的 APIError 类抛出错误。

auth.ts
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";

export const auth = betterAuth({
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          if (user.isAgreedToTerms === false) {
            // 你的特殊条件
            // 抛出 API 错误
            throw new APIError("BAD_REQUEST", {
              message: "用户必须同意服务条款后才能注册。",
            });
          }
          return {
            data: user,
          };
        },
      },
    },
  },
});

使用上下文对象

上下文对象 (ctx) 作为钩子第二个参数传入,包含有用信息。对于 update 钩子,ctx 包含当前 session,可访问已登录用户详情。

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  databaseHooks: {
    user: {
      update: {
        before: async (data, ctx) => {
          // 可通过 ctx 访问 session
          if (ctx.context.session) {
            console.log("用户更新由以下用户发起:", ctx.context.session.userId);
          }
          return { data };
        },
      },
    },
  },
});

数据库钩子和标准钩子一样,也提供丰富的 ctx 对象属性。详见 钩子文档

插件架构

插件可以定义数据库中的自有表以存储更多数据,也可向核心表添加字段存储额外信息。例如,二步验证插件会向 user 表添加如下字段:

  • twoFactorEnabled:用户是否启用二步验证。
  • twoFactorSecret:用于生成 TOTP 代码的密钥。
  • twoFactorBackupCodes:账户恢复的加密备用代码。

要向数据库添加表和列,你有两种选择:

  • CLI:使用 migrate 或 generate 命令。命令会扫描数据库并指导你添加缺失的表或列。
  • 手动方法:按照插件文档说明手动添加表和列。

两种方式均确保数据库架构与插件需求保持同步。

实验性联接(Joins)

自 Better Auth 1.4 版本起,新增了实验性的数据库联接支持。 这允许 Better Auth 在单个请求中执行多条数据库查询,减少数据库往返次数。 超过 50 个端点支持联接,我们也在持续添加中。

在内部,我们的适配器系统原生支持联接,即使你未启用实验性联接, 它仍会退回为多条查询并合并结果的方式执行。

启用联接,请在认证配置中加入:

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  experimental: { joins: true }
});

Better Auth 1.4 CLI 会为 DrizzleORM 和 PrismaORM 自动生成关联关系,所以如果你尚未添加,请运行 migrate 或 generate 命令更新到最新所需架构。

强烈建议阅读对应适配器关于实验性联接的文档: