# Next.js + Supabase + Cloudflare worker + Hyperdrive 最佳实践(2026终极版)
2026-02-1917 分钟阅读[cloudflare worker](https://meathill.com/category/cloudflare-worker)[next.js](https://meathill.com/tag/next-js)[cloudflare](https://meathill.com/tag/cloudflare)[supabase](https://meathill.com/tag/supabase)[cloudflare worker](https://meathill.com/tag/cloudflare-worker)[hyperdrive](https://meathill.com/tag/hyperdrive)

> 再见 Vercel,你好 Cloudflare Worker。
前些天,一位朋友找到我,因为 Vercel 太贵,她想把一个网站迁移到 Cloudflare。其它问题似乎 AI 和她请的程序员都搞定了,但是由于之前使用的是 Supabase 数据库,而 Cloudflare Worker 不支持 Supabase,所以她咨询我应该迁移到什么数据库。
我的第一反应是不可能,绝对不可能。这都 2026 年,这么出名的 Supabase,Cloudflare Worker 不可能不支持。搞不定就丢给我,我分分钟拿下。
实际上,为了让她的 [3D 模型处理服务](https://ai3dmodel.app/) 成功跑在 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终极版)**](https://meathill.com/en/posts/next-js/best-practice-for-nextjs-on-cloudflare-worker-2026)
在 Vercel(或者标准的 Node.js 环境)里,我们习惯了这样拿数据库连接串:
```typescript
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()` 获取:
```typescript
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)初始化数据库连接了**。
之前我们可以:
```typescript
// global.ts
export const db = drizzle(client); // 全局单例,直接导出
```
现在如果你在顶层调用 `getCloudflareContext()`,它会抛错或者返回 undefined,因为模块加载时还没有请求进来。
## 第二关:Hyperdrive 真正的奥义:连接池
解决了环境变量,下一步就是连接数据库。
Cloudflare Hyperdrive 不仅可以加速全球访问,速度更快;实际上它对于 Serverless 架构来说,更核心的作用是维护 **连接池(Connection Pooling)**。
### 数据库面对 Serverless 的噩梦:连接数耗尽
简单解释一下:传统的 Node.js 服务是长驻的,启动服务后就会建立数据库连接池(比如 `max: 10`),这个应用里所有请求共用这 10 个连接。
但是 Cloudflare Workers 是 **Serverless** 的,这意味着:
1. 流量来了,瞬间启动几千个 Worker 实例。
2. 每个 Worker 实例都要去连数据库。
3. 如果不加控制,瞬间几千个连接打到你的 Postgres 数据库上。
4. 数据库直接挂掉("Too many connections")。
5. 对于 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(可能是协议握手的问题?)。
这引出了三个连锁反应的坑:
1. **必须用直连 Direct Connection**:配置 Hyperdrive 时,Host 必须是 Supabase 的直连地址(特征:端口 5432)。
2. **本地开发连不上**:Supabase 的免费版(Free Tier)直连地址只支持 IPv6。如果你的本地网络环境或者开发工具只支持 IPv4,你在本地是死活连不上这个 Direct 地址的。对我来说,由于连不上,我以为这个连接 URL 有问题,直接放弃使用它,换用 6543 pooler,导致后面一系列反复的问题。
3. **必须关闭 SSL**:这里也很反直觉。通常连接云数据库必须开 SSL (`ssl: 'require'`)。但是,Hyperdrive 和你的 Worker 都在 Cloudflare 内网,**必须显式关闭 SSL**:
```typescript
// 这里 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 过程中我发现,如果按照惯例缓存数据库连接实例反复使用,就会遇到这个错误:
```bash
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](https://opennext.js.org/cloudflare/howtos/db) 后,了解到不能全局缓存数据库连接实例,必须每个请求——这里的请求指 **用户的请求**,即一次用户访问产生的数据库连接请求,比如同一个 API 里执行 3 次查询,可以复用同一个连接——都需要创建新的连接实例才行。
具体到代码上,是这样的。在 Vercel/Node 时代,为了防止 serverless function 每次反复连接数据库把连接池撑爆,通常来说我们会**使用全局单例**:
```typescript
// 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'`。
**但是**,结合前面提到的两点:
1. 必须在请求上下文 (`getCloudflareContext`) 里才能拿到真实连接串。
2. Hyperdrive 的连接建议是“随用随连”(轻量级)。
我们不得不把 `db` 从一个**全局变量**变成一个**请求级函数**。
```typescript
// 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:**
```typescript
import { db } from '@/lib/db';
export async function getUser(id: string) {
return await db.query.users.findFirst({ ... });
}
```
**After:**
```typescript
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 总攻略:
1. 修改环境变量,妥善利用 `getCloudflareContext().env`。
2. 使用 Supabase 直连 URL(端口5432) 配置 Hyperdrive,创建数据库连接
3. 对于本地开发,直接使用 Supabase 的 pooler URL(端口 6543)创建数据库连接
4. 把全局 `db` 单例重构成 `await getDb()` ,然后按需获取
最后,我还是帮朋友把代码仓库修好了。Cloudflare Workers 的冷启动几乎可以忽略不计,配合 Hyperdrive,数据库查询速度在边缘节点也相当可观。最重要的是,再也不用担心 Next.js 部署在 Vercel 上的高昂账单了。
如果你也在做类似的迁移,希望上面的经验总结能帮你一次性解决问题。