为 Cloudflare Worker 个人项目添加 better-auth 支持
2025 年 09 月 01 日
背景
Cloudflare workers 属于 serverless 的范畴,本质是利用了 cloudflare 的边缘服务器来运行一段代码。
Cloudflare workers 的运行环境被称为 v8 isolate,缺少部分 nodejs 的 API。
由于 Cloudflare 同时提供了 D1/R2/KV 等数据库或类数据库服务,因此在 Cloudflare workers 上可以实现一个轻量的后端。个人项目的使用量不大,足以免费使用。
认证是后端最常见的功能之一,而且实现起来比较复杂。better-auth 是目前最流行的认证框架(可能没有之一)。但 better-auth 默认没有直接支持 Cloudflare D1,因此无法简单整合到 Cloudflare workers 项目中。
本文实现了一个在 Cloudflare workers 上,使用 better-auth 进行认证的工程。参考:better-auth-cloudflare。读者需要对 Cloudflare workers 的开发方式有一些基础的理解(比如 wrangler CLI 的使用等)。
创建工程
执行命令(工程名明显可以自定义):
npm create cloudflare@latest -- my-first-worker
注:如果用 bun,注意 bunx 和 bun 版本是否一致。
考虑到工程需要前端,可以选择 framework starter。
另注:如果选 js 而不是 ts,在默认工程的
wrangler.jsonc
文件中的默认 main 入口点可能会出错,需手动修改。
以 vue 框架工程为例,初始目录如下:
.
├── index.html
├── jsconfig.json
├── package.json
├── package-lock.json
├── public
├── README.md
├── server
├── src
├── vite.config.js
└── wrangler.jsonc
比普通的 vue 工程只多了 server
目录和 wrangler.jsonc
文件。也就是后端接口实现。
成功创建工程后,npm install
再 npm run dev
即可正常调试。
创建 D1 数据库
执行命令(需自定义数据库名):
wrangler d1 create my-first-worker-db # 该命令仅生成远端数据库,不生成本地数据库
会得到一段配置,加到 wrangler.jsonc
中。注意配置中要增加 migration 配置:"migrations_dir": "drizzle"
。表示让 wrangler 后续从 drizzle 目录读取 migration sql。
然后执行一条无意义的 SQL 命令来生成本地数据库(后面有任务需要),例如:
wrangler d1 execute my-first-worker-db --local --command "SELECT 1;"
注:wrangler 会把生成的本地数据库放在
.wrangler/state/v3/d1/miniflare-D1DatabaseObject/
目录。考虑到 drizzle 配置为查找该目录下第一个 sqlite 文件,因此请注意保持该目录下的 sqlite 文件仅有一个。
添加认证功能
要使用 better-auth 来添加认证功能,需要按照后端和前端来考虑问题。其本质是,better-auth 为后端提供 /api/auth/**
的服务端 API,然后在前端提供对应的客户端 API。
后端部分:better-auth 适配 D1 的初始化方法
better-auth 提供了一个 CLI,该 CLI 通过 auth.ts
文件(参考官方文档的 Database 章节)来生成数据库的 schema/migration。
但是,由于 D1 仅可在 Cloudflare workers 环境下访问,而 better-auth 的 CLI 需要在 node.js 环境下工作。两者明显不是一套环境。因此要用一种变通方法实现 auth.ts/auth.js
,也就是同时适配 worker 环境和 node.js 环境。
数据库选择 drizzle-orm 来适配 D1,首先需要实现以下几个文件:
- server/auth.js:为后端创建认证中间件。
- server/db/index.ts:数据库相关代码。
- server/db/schema.ts:drizzle 所需的数据库 schema。
- server/db/auth-schema.ts:数据库中认证部分的 schema,可以通过命令生成。
// server/auth.js, 注意这是调试用的 betterAuth 配置,后续要根据需求调整
import { betterAuth } from 'better-auth'
import { drizzle } from 'drizzle-orm/d1'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { schema } from './db'
// Single auth configuration that handles both CLI and runtime scenarios
function createAuth(env) {
// Use actual DB for runtime, empty object for CLI
const disableSignUp = !!env?.DISABLE_SIGN_UP
const db = env ? drizzle(env.DB, { schema, logger: true }) : {}
return betterAuth({
database: drizzleAdapter(db, {
provider: 'sqlite', // or "mysql", "sqlite"
}),
emailAndPassword: {
enabled: true,
disableSignUp,
requireEmailVerification: false,
},
})
}
// Export for CLI schema generation
export const auth = createAuth()
// Export for runtime usage
export { createAuth }
// server/db/index.ts
import { schema } from "./schema";
// Re-export the drizzle-orm types and utilities from here for convenience
export * from "drizzle-orm";
// Re-export the feature schemas for use in other files
export * from "./auth-schema"; // Export individual tables for drizzle-kit
export * from "./schema";
// server/db/schema.ts
import * as authSchema from './auth-schema' // This will be generated in a later step
// Combine all schemas here for migrations
export const schema = {
...authSchema,
// ... your other application schemas
}
上面三个文件实现了 drizzle 以及 schema 的基础配置。另外需要注意,server/db/auth-schema.ts
文件会在后续步骤生成,在现在这个时间点,可以先生成一个空文件。
接下来,执行命令来生成 auth-schema.ts:
npx @better-auth/cli@latest generate --output ./server/db/auth-schema.ts -y
然后,需要一个 /drizzle.config.ts
来辅助生成 drizzle 的 migration 脚本。很明显,代码需要寻找本地数据库,因此需要提前创建好。
// /drizzle.config.ts
import { defineConfig } from "drizzle-kit";
import fs from "node:fs";
import path from "node:path";
function getLocalD1DB() {
try {
const basePath = path.resolve(".wrangler");
const dbFile = fs
.readdirSync(basePath, { encoding: "utf-8", recursive: true })
.find(f => f.endsWith(".sqlite"));
if (!dbFile) {
throw new Error(`.sqlite file not found in ${basePath}`);
}
const url = path.resolve(basePath, dbFile);
return url;
} catch (err) {
console.log(`Error ${err}`);
}
}
export default defineConfig({
dialect: "sqlite",
schema: "./server/db/index.ts",
out: "./drizzle",
...(process.env.NODE_ENV === "production"
? {
driver: "d1-http",
dbCredentials: {
accountId: process.env.CLOUDFLARE_D1_ACCOUNT_ID,
databaseId: process.env.CLOUDFLARE_DATABASE_ID,
token: process.env.CLOUDFLARE_D1_API_TOKEN,
},
}
: {
dbCredentials: {
url: getLocalD1DB(),
},
}),
});
然后生成 drizzle 的 migration 并执行 migration:
npx drizzle-kit generate # 根据 /server/db/schema.ts,在 drizzle 目录下生成 migration sql
执行 sql migration,将表结构写入数据库,本地操作:
wrangler d1 migrations apply --local my-first-worker-db
现在本地数据库里应该已经能查看到对应的表结构了。然后完善 /server/index.js
入口点,注意提前安装好 hono。
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { createAuth } from './auth'
// Note: configure ALLOWED_ORIGINS in production environment, e.g. "http://example.com,https://app.example.com"
const app = new Hono()
// CORS configuration for auth routes
app.use(
'/api/auth/**',
cors({
// prettier-ignore
origin: (origin, c) => {
// read allowed origins from environment variable
const allowedOrigins = (c.env.ALLOWED_ORIGINS || '').split(',').map((o) => o.trim()).filter(Boolean)
// when ALLOWED_ORIGINS not configured, allow all
if (allowedOrigins.length === 0) { return '*' }
// check if origin allowed and return
return (origin && allowedOrigins.includes(origin)) ? origin : null
},
allowHeaders: ['Content-Type', 'Authorization'],
allowMethods: ['POST', 'GET', 'OPTIONS'],
exposeHeaders: ['Content-Length'],
maxAge: 600,
credentials: true,
})
)
// Middleware to initialize auth instance for each request
app.use('*', async (c, next) => {
const auth = createAuth(c.env)
c.set('auth', auth)
await next()
})
// Handle all auth routes
app.all('/api/auth/*', async (c) => {
const auth = c.get('auth')
return auth.handler(c.req.raw)
})
// 对于受保护接口,使用 c.get("auth") 获取认证中间件后,通过 session 确认登录状态,再执行后续操作
export default app
接下来用 curl 命令模拟测试,先模拟注册命令:
curl -X POST http://localhost:5173/api/auth/sign-up/email -H "Content-Type: application/json" -d '{"email":"[email protected]","password":"12345678","name":"Tester"}'
然后模拟登录:
curl -X POST http://localhost:5173/api/auth/sign-in/email -H "Content-Type: application/json" -d '{"email":"[email protected]","password":"12345678"}'
前端部分:使用 better-auth 完成认证
前端部分现在可以很容易的添加 better-auth 认证功能了。首先是增加一个 /src/lib/auth-client.ts
。
// /src/lib/auth-client.ts
import { createAuthClient } from "better-auth/vue"
export const authClient = createAuthClient({})
然后简单修改之前的 App.vue,在 <script setup>
段修改,参考下面一段代码:
import { authClient } from './lib/auth-client'
const session = authClient.useSession() // 获取 session,这是一个 Proxy 类型变量
// 修改原有的 getName 实现,比如改成下面这样:
const getName = async () => {
if (session.value.data) { // 如果已经登录则登出
await authClient.signOut()
return
}
// 如果没有登录则用之前注册的用户登录
await authClient.signIn.email({
email: '[email protected]',
password: '12345678',
rememberMe: true,
})
}
现在可以容易的从 session 中获取当前用户,例如这样修改原来的按钮:
<button class="green" @click="getName" aria-label="get name">Name from API is: {{ session?.data?.user?.name || "Unknown" }}</button>
上面只是一个简单示例,实际功能的完整实现要对 vue 工程进行完善。
其他
剩下的功能实现基本就跟传统开发完全一样了,前端用 vue router 扩展界面;后端添加受保护接口,使用 drizzle 扩展数据库支持并操作数据库。
开发完成后,执行数据库 migration,然后发布。
后续的 Deploy to Cloudflare 按钮功能还需要进一步研究。
注:如果只希望保留自己作为唯一用户,那么注册后可以通过环境变量 DISABLE_SIGN_UP (参考前面的例子代码),关掉注册功能。记得要重新发布一次。