Next.js + Supabase + Cloudflare worker + Hyperdrive 最佳实践(2026终极版)
再见 Vercel,你好 Cloudflare Worker。
前些天,一位朋友找到我,因为 Vercel 太贵,她想把一个网站迁移到 Cloudflare。其它问题似乎 AI 和她请的程序员都搞定了,但是由于之前使用的是 Supabase 数据库,而 Cloudflare Worker 不支持 Supabase,所以她咨询我应该迁移到什么数据库。
我的第一反应是不可能,绝对不可能。这都 2026 年,这么出名的 Supabase,Cloudflare Worker 不可能不支持。搞不定就丢给我,我分分钟拿下。
实际上,为了让她的 3D 模型处理服务 成功跑在 Cloudflare Worker 上,我经历了三场漫长的 Debug 战役,把整个数据库连接层重写了 N 遍。
如果你也正打算使用 Cloudflare worker + Supabase,也在使用 AI 开发,那你也可能会踩进 Cloudflare Worker + Hyperdrive + Supabase 的坑。我建议你认真阅读这篇文章,然后把地址保存下来,将来开发的时候,把它丢给 AI,应该可以帮你节省很多时间。
为什么要逃离 Vercel?
Vercel 很好,Developer Experience 极佳。但是当你的用户量开始增长,你会发现 Vercel 的账单成长得更快……你此刻可能还没开始挣钱,但 Vercel 不管那些,看着每个月几十上百的账单,你会不由得焦虑起来……
Cloudflare Workers 提供了极高的性价比和几乎无限的并发能力,区区 $5/m,媲美 Vercel $100/m 以上的额度;还有 Hyperdrive —— 那个声称能让你的数据库连接像开了加速器一样的神奇功能。
是否应该选择 Supabase?
如果你只是需要一个数据库,我其实不太推荐 Supabase。相对来说,Supabase 是个非常强力的一站式解决方案,除了 Serverless 数据库,它还支持 Auth 和 Edge Function,可以用一个工具满足很多需求,尤其是开发移动 App 的时候,非常方便。
但如果你并不了解 Serverless 数据库应该怎么开发应用,或者只是需要一个关系型数据库,那么 Supabase 提供的好处你恐怕享受不到,而它的免费额度要远低于 D1 或者 TiDB Cloud。所以我起初的想法是,搞不定我就帮朋友迁移到 TiDB。后来也是上头了才开始跟 Hyperdrive + Supabase 死磕。
第一关:消失的环境变量
关于环境变量,我写过一篇更详尽的文章,建议你阅读:Cloudflare Worker + Next.js 使用环境变量最佳实践(2026终极版)
在 Vercel(或者标准的 Node.js 环境)里,我们习惯了这样拿数据库连接串:
import postgres from 'postgres';
const connectionString = process.env.DATABASE_URL;
const client = postgres(connectionString)
export const db = drizzle(client, config);
代码写完,本地 next dev 一跑,完美。部署到 Cloudflare Worker,报错……
原因:Cloudflare Workers 的环境变量机制和 Node.js 不同。
在 Workers 运行时里,Hyperdrive 是 binding,并不是典型的环境变量,自然也不会挂在 process.env 上,而是通过 env 对象传递给请求的上下文。特别是当你使用了 @opennextjs/cloudflare 适配器时,你需要显式地去获取上下文。
并不是简单的 process.env
你需要把所有直接读取 process.env 的代码,改成通过 getCloudflareContext() 获取:
import { getCloudflareContext } from '@opennextjs/cloudflare';
// 必须在函数内部调用,因为只有其实在处理请求时才有 Context
function getConnectionInfo() {
const runtimeEnv = getCloudflareContext().env;
// 现在的 runtimeEnv 里才真正包含了你在 Cloudflare Dashboard 或者 wrangler.toml 配置的 vars 和 secrets,以及 bindings
// 然后再把 hyperdrive 拿出来创建实例
const hyperdrive = runtimeEnv.HYPERDRIVE as { connectionString?: string } | undefined;
if (hyperdrive?.connectionString) {
return { connectionString: hyperdrive.connectionString, source: 'hyperdrive' };
}
const localHyperdrive = runtimeEnv.CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE;
if (localHyperdrive) {
return { connectionString: localHyperdrive, source: 'hyperdrive-local' };
}
throw new Error('HYPERDRIVE is not configured');
}
这带来了一个巨大的架构变动:你不能在文件顶层(Top Level)初始化数据库连接了。
之前我们可以:
// global.ts
export const db = drizzle(client); // 全局单例,直接导出
现在如果你在顶层调用 getCloudflareContext(),它会抛错或者返回 undefined,因为模块加载时还没有请求进来。
第二关:Hyperdrive 真正的奥义:连接池
解决了环境变量,下一步就是连接数据库。
Cloudflare Hyperdrive 不仅可以加速全球访问,速度更快;实际上它对于 Serverless 架构来说,更核心的作用是维护 连接池(Connection Pooling)。
数据库面对 Serverless 的噩梦:连接数耗尽
简单解释一下:传统的 Node.js 服务是长驻的,启动服务后就会建立数据库连接池(比如 max: 10),这个应用里所有请求共用这 10 个连接。
但是 Cloudflare Workers 是 Serverless 的,这意味着:
- 流量来了,瞬间启动几千个 Worker 实例。
- 每个 Worker 实例都要去连数据库。
- 如果不加控制,瞬间几千个连接打到你的 Postgres 数据库上。
- 数据库直接挂掉("Too many connections")。
- 对于 Supabase 这样的云数据库,就是直接拒绝访问。
Hyperdrive 的基本设计思路是:Worker 不直接连真实的数据库,而是连 Cloudflare 并在全球边缘节点部署的 Hyperdrive 代理。这些代理维护着到真实数据库的长连接池。你的 Worker 连 Hyperdrive 极快(内网级别),而 Hyperdrive 帮你复用在那有限的几十个真实数据库连接上。
这样一来,即使你有很多个应用,也不用考虑应用间的连接池共享,因为 Cloudflare 都帮你维护着。
Supabase 的坑中坑
朋友的数据库是 Supabase,Supabase 为了不要动不动就被阻塞,自己也提供连接池(Transaction Pooler),而且默认只让用户使用连接池(连接 URL 中包含 pooler 字眼,使用 6543 端口)。
直觉告诉我:既然要连接池,那我用 Hyperdrive 连 Supabase 的 Pooler 岂不是双倍快乐?
现实告诉我:报错。
Hyperdrive 必须连接数据库的 Direct Connection(通常是 port 5432)。它自己就是一个 Pooler,它不支持去连另一个 Pooler(可能是协议握手的问题?)。
这引出了三个连锁反应的坑:
- 必须用直连 Direct Connection:配置 Hyperdrive 时,Host 必须是 Supabase 的直连地址(特征:端口 5432)。
- 本地开发连不上:Supabase 的免费版(Free Tier)直连地址只支持 IPv6。如果你的本地网络环境或者开发工具只支持 IPv4,你在本地是死活连不上这个 Direct 地址的。对我来说,由于连不上,我以为这个连接 URL 有问题,直接放弃使用它,换用 6543 pooler,导致后面一系列反复的问题。
- 必须关闭 SSL:这里也很反直觉。通常连接云数据库必须开 SSL (
ssl: 'require')。但是,Hyperdrive 和你的 Worker 都在 Cloudflare 内网,必须显式关闭 SSL:
// 这里 cache 的作用下一节会解释
export const getDb = cache(() => {
const { connectionString, source } = getConnectionInfo();
return createDatabase({
connectionString,
// Hyperdrive 相当于内网,没有 SSL,必须关掉!
// 只有直连真实数据库(比如 build 阶段)才启用 require
enableSSL: source === 'hyperdrive' ? false : 'require',
});
});
我们在 DEBUG 期间被这个问题折磨了很久:明明连接串是对的,明明 Supabase 设置也没问题,但就是握手失败。尤其使用 AI 开发一定要小心,因为 AI 很可能会顺手就加上 SSL。
第三关:每个连接用一次,用完即弃
debug 过程中我发现,如果按照惯例缓存数据库连接实例反复使用,就会遇到这个错误:
The Workers runtime canceled this request because it detected that your Worker's code had hung and would never generate a response. Refer to: https://developers.cloudflare.com/workers/observability/errors/
表现为没有响应,到达设置的时限之后报超时错误。
后来认真阅读 OpenNext 官方文档 Cloudflare > How-Tos > Database & ORM 后,了解到不能全局缓存数据库连接实例,必须每个请求——这里的请求指 用户的请求,即一次用户访问产生的数据库连接请求,比如同一个 API 里执行 3 次查询,可以复用同一个连接——都需要创建新的连接实例才行。
具体到代码上,是这样的。在 Vercel/Node 时代,为了防止 serverless function 每次反复连接数据库把连接池撑爆,通常来说我们会使用全局单例:
// Before: Global Singleton
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
// 无论 import 多少次,client 和 db 只有一份
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client);
然后你在业务代码里随心所欲地 import { db } from '@/lib/db'。
但是,结合前面提到的两点:
- 必须在请求上下文 (
getCloudflareContext) 里才能拿到真实连接串。 - Hyperdrive 的连接建议是“随用随连”(轻量级)。
我们不得不把 db 从一个全局变量变成一个请求级函数。
// After: Function based
// 使用 React Cache 确保同一个请求内只创建一次以复用
// 这里可以是 async function,主要看是否在 Server component 里使用
export const getDb = cache(() => {
const { connectionString, source } = getConnectionInfo();
// 创建连接
return createDatabase({
connectionString,
// Hyperdrive 不需要 SSL,这里根据本地开发或线上部署切换 'require'
enableSSL: source === 'hyperdrive' ? false : 'require',
});
});
有点痛苦的重构过程
这一改,意味着整个项目里几百个 import { db } from '@/lib/db' 全部失效。这是需要最大规模重构的部分,不过因为有 AI 存在,所以其实还好。
所有的 Server Action、API Route、Data Access Layer 全部要改:
Before:
import { db } from '@/lib/db';
export async function getUser(id: string) {
return await db.query.users.findFirst({ ... });
}
After:
import { getDb } from '@/lib/db';
export async function getUser(id: string) {
const db = await getDb(); // <--- 每一处都要加这个
return await db.query.users.findFirst({ ... });
}
不仅仅是加一行代码,由于 db 现在是异步获取或者在函数内部获取,很多原本直接导出 query 对象的工具函数也得重写。
总结
虽然过程很痛苦,但问题终归是解决了。以下是使用 Cloudflare Worker + Supabase 总攻略:
- 修改环境变量,妥善利用
getCloudflareContext().env。 - 使用 Supabase 直连 URL(端口5432) 配置 Hyperdrive,创建数据库连接
- 对于本地开发,直接使用 Supabase 的 pooler URL(端口 6543)创建数据库连接
- 把全局
db单例重构成await getDb(),然后按需获取
最后,我还是帮朋友把代码仓库修好了。Cloudflare Workers 的冷启动几乎可以忽略不计,配合 Hyperdrive,数据库查询速度在边缘节点也相当可观。最重要的是,再也不用担心 Next.js 部署在 Vercel 上的高昂账单了。
如果你也在做类似的迁移,希望上面的经验总结能帮你一次性解决问题。