ByteNoteByteNote

字节笔记本

2026年2月20日

Next.js + Supabase + Cloudflare Worker + Hyperdrive 最佳实践(2026终极版)

本文分享从 Vercel 迁移到 Cloudflare Worker + Supabase + Hyperdrive 的完整实战经验,包括环境变量处理、连接池配置和数据库连接重构等关键问题。

为什么要逃离 Vercel?

Vercel 很好,Developer Experience 极佳。但是当用户量开始增长,你会发现 Vercel 的账单成长得更快……看着每个月几十上百的账单,你会不由得焦虑起来。

Cloudflare Workers 提供了极高的性价比和几乎无限的并发能力,区区 $5/月,媲美 Vercel $100/月以上的额度;还有 Hyperdrive —— 那个声称能让你的数据库连接像开了加速器一样的神奇功能。

是否应该选择 Supabase?

如果你只是需要一个数据库,其实不太推荐 Supabase。相对来说,Supabase 是个非常强力的一站式解决方案,除了 Serverless 数据库,它还支持 Auth 和 Edge Function。

但如果你并不了解 Serverless 数据库应该怎么开发应用,或者只是需要一个关系型数据库,那么 Supabase 提供的好处你恐怕享受不到,而它的免费额度要远低于 D1 或者 TiDB Cloud。

第一关:消失的环境变量

并不是简单的 process.env

在 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 的代码,改成通过 getCloudflareContext() 获取:

typescript
import { getCloudflareContext } from '@opennextjs/cloudflare';

// 必须在函数内部调用,因为只有其实在处理请求时才有 Context
function getConnectionInfo() {
  const runtimeEnv = getCloudflareContext().env;

  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 真正的奥义:连接池

数据库面对 Serverless 的噩梦:连接数耗尽

简单解释一下:传统的 Node.js 服务是长驻的,启动服务后就会建立数据库连接池(比如 max: 10),这个应用里所有请求共用这 10 个连接。

但是 Cloudflare Workers 是 Serverless 的,这意味着:

  1. 流量来了,瞬间启动几千个 Worker 实例
  2. 每个 Worker 实例都要去连数据库
  3. 如果不加控制,瞬间几千个连接打到你的 Postgres 数据库上
  4. 数据库直接挂掉("Too many connections")

Hyperdrive 的基本设计思路:Worker 不直接连真实的数据库,而是连 Cloudflare 并在全球边缘节点部署的 Hyperdrive 代理。这些代理维护着到真实数据库的长连接池。你的 Worker 连 Hyperdrive 极快(内网级别),而 Hyperdrive 帮你复用在那有限的几十个真实数据库连接上。

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 地址的
  3. 必须关闭 SSL:Hyperdrive 和你的 Worker 都在 Cloudflare 内网,必须显式关闭 SSL
typescript
export const getDb = cache(() => {
  const { connectionString, source } = getConnectionInfo();

  return createDatabase({
    connectionString,
    // Hyperdrive 相当于内网,没有 SSL,必须关掉!
    enableSSL: source === 'hyperdrive' ? false : 'require',
  });
});

第三关:每个连接用一次,用完即弃

如果按照惯例缓存数据库连接实例反复使用,就会遇到这个错误:

The Workers runtime canceled this request because it detected that your Worker's code had hung and would never generate a response.

表现为没有响应,到达设置的时限之后报超时错误。

解决方案:不能全局缓存数据库连接实例,必须每个请求都需要创建新的连接实例才行。

重构前后对比

Before: Global Singleton

typescript
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);

After: Function based

typescript
// 使用 React Cache 确保同一个请求内只创建一次以复用
export const getDb = cache(() => {
  const { connectionString, source } = getConnectionInfo();

  return createDatabase({
    connectionString,
    enableSSL: source === 'hyperdrive' ? false : 'require',
  });
});

业务代码改动

所有的 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({ ... });
}

总结

使用 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 上的高昂账单了。


来源: 山维空间 (Meathill) 发布时间: 2026-02-19 原文链接: https://meathill.com/cloudflare-worker/nextjs-supabase-cloudflare-worker-hyperdrive

分享: