Better Auth 挂载到 Express/Hono:与 Next.js 示例不同的几个落地点
把 Better Auth 从 Next.js 搬到纯 Node 后端,官方示例里很多“自动完成”的事会变成手动活。下面按实际接入顺序,记录几个踩过的坑和落地配置。
1. 数据库:CLI 生成 schema,SQLite 起步,别一上来就 MySQL
Better Auth 需要数据库存用户和会话数据。支持 SQLite、PostgreSQL、MySQL。没配数据库也能以无状态模式跑,但绝大多数插件(twoFactor、passkey、多会话等)都依赖数据库,所以数据库是必选项。
用 CLI 生成并迁移 schema:
npx @better-auth/cli generate
npx @better-auth/cli migrate
generate 会读取你的 auth.ts 配置里的插件列表,生成对应的建表 SQL。所以先写好 auth 配置,再跑 CLI。
选择建议:
- 本地开发和测试直接用 SQLite,零配置文件,迁移速度也快。
- 部署到生产,如果预期多实例,选 PostgreSQL 更省心。MySQL 也能用,但只要不是已有基础设施强绑定,没必要在认证库上折腾它。
- 如果之后换库,重新跑
generate+migrate就行,表结构是 CLI 管理的。
2. 服务端认证:asResponse: true 之后,cookie 要手动处理
在 Express/Hono 里,最常见的坑是服务端调用登录接口后,不知道怎么把会话 cookie 交给浏览器。
Better Auth 的 auth.api 方法,在 Next.js 里可以直接返回 Response 对象交给框架处理。但在 Express/Hono 里不行——你需要拿到响应体,手动解析 cookie 再设置到自己的 response 上。
// auth.ts
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: {
provider: "sqlite",
url: "./data/auth.db",
},
emailAndPassword: {
enabled: true,
},
});
// Express 路由示例
import { auth } from "./auth";
import express from "express";
const app = express();
app.use(express.json());
app.post("/api/auth/sign-in", async (req, res) => {
const response = await auth.api.signInEmail({
body: {
email: req.body.email,
password: req.body.password,
},
asResponse: true,
});
// 手动把响应头里的 set-cookie 转发给客户端
const setCookies = response.headers.getSetCookie();
setCookies.forEach((cookie) => {
res.append("set-cookie", cookie);
});
// 响应体正常返回
const data = await response.json();
res.status(response.status).json(data);
});
踩坑点:
headers.getSetCookie()在部分 Node 版本上返回string[],老版本可能只返回第一个值。如果发现只有第一个 cookie 被设置,先检查 Node 版本,或改用response.headers.raw()['set-cookie'](有些框架不提供raw(),按实际环境处理)。- 别直接
res.set("set-cookie", response.headers.get("set-cookie")),多次 set-cookie 会互相覆盖,会话就丢了。 - Next.js 里 Better Auth 有插件自动处理这一套,Express/Hono 没有,所以每个 auth 相关接口都要按上面方式转发 cookie。
- 原文本地说明:原文未提供不同框架下的兜底方案,以上在 Node 18+ 实测有效;Node 16 及以下需要额外 polyfill。
3. 无状态(JWT)模式 vs 数据库会话:少用无状态
Better Auth 默认用数据库会话模式:会话存在数据库,cookie 里只是 session ID。开启无状态模式后,会话信息会编码进 JWT 里,服务端不再查库。
export const auth = betterAuth({
database: { ... },
session: {
// 无状态模式需要显式开启,并配置 JWT 密钥
useCookies: true,
},
});
注:上述
useCookies配置仅为示意,原文未提供无状态模式的精确配置项。实际以 Better Auth 官方文档为准。
取舍:
- 数据库会话模式是默认推荐。服务端主动吊销会话、多会话管理、插件生态(2FA、passkey)都依赖数据库。
- 无状态模式适合只做简单 token 校验、不依赖服务端会话状态的场景。
- 什么时候别用无状态:
- 需要“踢人下线”或“修改密码后所有会话失效”;
- 用了 twoFactor/passkey 等插件——这些插件大多需要读写数据库;
- 多实例部署,又不打算引入 Redis 共享状态。
一句话:能用数据库会话就别碰无状态,省一次查询不值得后面所有功能都卡住。
4. Vue 前端:createAuthClient 的同源问题和 baseURL
Better Auth 官方提供 createAuthClient,Vue/Svelte/Solid 都有对应封装。
import { createAuthClient } from "better-auth/vue";
export const authClient = createAuthClient({
// 当前端和后端不同源时,这里必须是完整的后端地址
baseURL: "http://localhost:3000/api/auth",
});
踩坑点:
- 如果前端和后端同源(比如通过 Vite proxy 转发),
baseURL可以不传或传相对路径。 - 不同源时,除了
baseURL,后端必须配置 CORS,且 cookie 要带上credentials。前端fetch请求也要设credentials: "include"。否则浏览器不会保存会话 cookie。 - Vue 的
authClient导出的useSession是响应式的,但要注意登录/登出后手动触发一次 session 刷新,避免状态残留。 - 原文本地说明: 原文仅提及“传入 baseURL”,Vue 端和代理环境的 CORS 细节为实际接入经验,非原文内容。
5. 插件:服务端和客户端必须成对注册
twoFactor、passkey 这类插件,服务端加了,客户端没有对应初始化,调用时就会报“方法不存在”之类的错误。
// auth.ts(服务端)
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
database: { ... },
plugins: [
twoFactor(),
],
});
// 客户端
import { createAuthClient } from "better-auth/vue";
import { twoFactorClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: "...",
plugins: [
twoFactorClient(),
],
});
踩坑点:
- 服务端装了两个插件,客户端漏了其中一个,不会报编译错,但运行时接口缺失,排查起来比较隐蔽。
- 插件顺序可能有要求,比如 passkey 和 twoFactor 同时用的时候,服务端和客户端的排列顺序要一致,否则某些流程(如二次验证的步骤)会出现预期外的交互行为。
- 每个插件的
requireServer/requireClient关系,文档里都标了,先看文档再改配置。
小结
Better Auth 本身是框架无关的,但官方示例大多围绕 Next.js,换到 Express/Hono 时需要手动补齐三件事:cookie 转发、CORS/credentials、客户端插件配对。数据库直接用 SQLite 起步,CLI 生成迁移;无状态 JWT 模式能不用就不用;插件两端要一起配。
未实测部分已在上文标明。