编写高质量的 Supabase Edge Functions:从请求处理器契约到 `withSupabase` 安全封装的完整实践指南
本文源自 Supabase 仓库中为 AI 编码助手(Cursor Rules)准备的 edge-functions.md,系统化整理了在 TypeScript/Deno 运行时中编写 Supabase Edge Functions 的 13 条核心准则与 5 个可直接落地的函数模板。无论你是要为自己的项目编写第一个边缘函数,还是希望用 AI 辅助生成安全、可维护的 Serverless 代码,读完本文后你都能掌握:标准请求处理器 export default { fetch } 的写法、基于 withSupabase 的一行式鉴权/客户端注入、npm:/jsr: 规范的依赖管理,以及多路由、后台任务、临时文件等边界场景的正确姿势。
一句话背景:Supabase Edge Functions 是什么
Supabase Edge Functions 是在 Deno JavaScript 运行时上运行的 Serverless 函数,用于处理鉴权回调、Webhook、图像/文档生成、AI 推理等需要贴近用户或第三方系统触发的逻辑。整个仓库本身(docs 搜索、RAG、og-image 等场景)也是由这些函数支撑的,例如 supabase/functions 目录下就运行着 search-embeddings 与 og-images 两个面向文档站的边缘函数。本文的 13 条准则,正是从这类真实生产函数中提炼出的"避免踩坑清单"。
准则 1:优先使用 Web API 与 Deno 核心 API
Edge Functions 运行在 Deno 上,能直接使用标准的 Web 平台 API,因此优先用内置能力代替第三方依赖:
- 用
fetch代替 Axios; - 用 WebSockets API 代替
node-ws; - 用
Response.json、Request、URL等标准接口处理 HTTP。
仓库里的真实例子可以佐证这一点:location 函数 直接使用 req.headers.get(...) 读取 x-forwarded-for 头,并用全局 fetch 调用 ipinfo.io 的 IP 定位 API;而 postgres-on-the-edge 函数 则直接基于 Response.json 组装返回值。减少依赖意味着更小的冷启动体积、更少的供应链风险,也更贴合 Serverless 的运行时约束。
准则 2:公共工具放进 _shared,用相对路径导入
当多个 Edge Functions 需要复用同一段工具逻辑时:
- 把共享代码放进
supabase/functions/_shared目录; - 通过相对路径导入,例如
import { AuthMiddleware } from '../_shared/jwt/default.ts'; - 严禁在 Edge Functions 之间产生相互依赖(每个函数应能独立部署)。
仓库中的 examples 项目就完整实践了这一点:custom-jwt-validation 从 ../_shared/jwt/default.ts 导入鉴权中间件,drizzle 复用 ../_shared/schema.ts 中的数据库表结构,unit-testing 复用 ../_shared/types.ts 类型定义。这种布局让每个函数保持单一入口,便于 CI 单独构建与部署。
准则 3 与 4:外部依赖必须显式加前缀并锁定版本
Deno 的模块解析默认不支持 Node 风格的裸模块名,因此:
- 不要使用裸标识符(bare specifiers)。需要外部依赖时,必须使用
npm:或jsr:前缀,例如@supabase/supabase-js应写作npm:@supabase/supabase-js; - 外部导入必须定义版本号,例如
npm:express应写作npm:express@4.18.2。
仓库内两个方向都能找到依据:新风格函数(如 location)使用 npm:@supabase/server@^1;老风格函数(如 postgres-on-the-edge)使用 jsr:@db/postgres@^0,并明确锁定了主版本。为稳定起见始终锁定版本,能避免上游发版导致的不可控行为变化。
准则 5:npm:/jsr: 优先,少用 CDN 直链
对于 deno.land/x、esm.sh、unpkg.com 等 CDN 上的包,准则给出的处理方式是:把 CDN 主机名替换为 npm: 标识符,统一走 npm:/jsr: 注册中心。
需要说明的是,本仓库存在历史遗留代码并不完全遵循这条建议——例如 supabase/functions/search-embeddings/index.ts 中仍能看到 import 'https://deno.land/x/xhr@0.2.1/mod.ts' 与 https://esm.sh/openai@3.1.0 的写法。这正说明该规则是写给新代码的演进方向:新函数(用 withSupabase 的模板)一律走 npm:/jsr:,老函数逐步迁移。写新代码时应直接遵循本条准则。
准则 6:需要时可用 Node 内置 API
Deno 提供 Node 兼容层,Node 的内置模块通过 node: 前缀导入:
import process from 'node:process'
import { randomBytes } from 'node:crypto'
触发条件是在 Deno 原生 API 存在缺口时才使用 Node API。node: 前缀让 Deno 运行时能正确识别并加载对应的 Node 兼容实现,同时保留将来迁移到纯 Node 环境的可能性。
准则 7:请求处理器契约——export default { fetch }
这是 Edge Functions 最重要的结构性约定。不要使用 import { serve } from "https://deno.land/std@0.168.0/http/server.ts",也不要使用 Deno.serve 自建服务器。正确姿势是导出一个带 fetch 方法的默认对象:
export default {
fetch: async (req: Request) => {
return Response.json({ message: 'Hello world' })
},
}
这是 Supabase Edge Functions 的请求处理器契约,同样无需修改即可运行在 Cloudflare Workers 与 Bun 上,具有极强的可移植性。对照仓库可以发现这种演进趋势:较新的示例函数(location、postgres-on-the-edge 等)全部采用 export default { fetch } 结构,而 search-embeddings 这类较早的函数仍用 Deno.serve,二者并存印证了契约的迁移路径。按准则第 8 条,正式函数还应把该 handler 用 withSupabase 包装起来。
准则 8:用 withSupabase 一次性解决鉴权、授权、客户端与 CORS
npm:@supabase/server@^1 导出的 withSupabase 封装器,把以下重复劳动收敛为一行代码:
- Authentication(认证):校验调用方的凭证;
- Authorization(授权):只放行符合你所声明
auth模式的调用方; - 预配置客户端:
ctx.supabase(按调用方 RLS 策略限定作用域)与ctx.supabaseAdmin(绕过 RLS); - CORS 处理:包括 preflight 预检请求。
你唯一需要做的决定是选择 auth 模式。基础用法如下:
import { withSupabase } from 'npm:@supabase/server@^1'
export default {
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
const { data, error } = await ctx.supabase.from('countries').select('*')
if (error) throw error
return Response.json({ data })
}),
}
依据调用方选择 auth 模式
| 调用方 | auth 取值 |
verify_jwt |
注入的客户端 |
|---|---|---|---|
已登录用户(Authorization 头携带 JWT) |
'user' |
true(默认,可省略) |
ctx.supabase(受 RLS 约束) |
Cron、worker、pg_net 或其他函数 |
'secret' |
false |
ctx.supabaseAdmin(绕过 RLS) |
| 公开客户端 | 'publishable' |
false |
ctx.supabase |
| 公开端点或外部 webhook(需在代码内自行校验) | 'none' |
false |
需要时用 ctx.supabaseAdmin |
对应到仓库证据:
- 需要登录用户访问的 location 函数 使用
withSupabase({ auth: 'user' }, ...),并在 config.toml 中保留verify_jwt = true(该文件里location段即如此配置); - 需要公开访问的函数(如
browser-with-cors、discord-bot、stripe-webhooks、telegram-bot)则统一在 config.toml 中声明verify_jwt = false。
关闭 JWT 校验的配置
对任何非 'user' 模式,都必须在 supabase/config.toml 中为对应函数显式设置 verify_jwt = false:
[functions.my-function]
verify_jwt = false
本仓库根目录的 supabase/config.toml 中就有这样的真实案例:[functions.search-embeddings] verify_jwt = false,表明该函数不依赖调用方 JWT(其鉴权在代码内通过 secret key 完成)。examples 项目下的 config.toml 更是把几十个函数的 verify_jwt 逐一显式声明,是理解该配置的最佳范本。注意:config.toml 中函数名是目录名(例如 simple-mcp-server 需配合 entrypoint 指定嵌套路径,见该文件中 [functions.simple-mcp-server] 段的写法)。
更细粒度的密钥鉴权
ctx.userClaims保存经过验证的用户身份信息;- 若只想接受某一把指定的命名密钥,使用
auth: 'secret:<name>'或auth: 'publishable:<name>'; - 公开端点使用
auth: 'none',仍能获得 CORS 处理与ctx.supabaseAdmin。
准则 9:认识内置环境变量(无需手动设置)
以下环境变量在本地与云端 Supabase 环境中都会被自动预填,用户无需手动配置:
SUPABASE_URL:项目 API 地址;SUPABASE_PUBLISHABLE_KEYS:可发布密钥(publishable keys)的 JSON 映射;SUPABASE_SECRET_KEYS:服务端密钥(secret keys)的 JSON 映射;SUPABASE_DB_URL:Postgres 连接串(用于直连数据库,如 postgres-on-the-edge 中的用法)。
withSupabase 会自动读取这些变量,因此优先依赖它而不是手工读 key。如果确实要绕过 SDK 手工读取某个 key,需要解析 JSON 映射并按名字索引:
const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
// 取默认 secret key
const defaultKey = SUPABASE_SECRET_KEYS['default']
publishable keys 通过 SUPABASE_PUBLISHABLE_KEYS 以同样方式解析。注意:示例里的早期函数(search-embeddings)还依赖旧的 SUPABASE_SERVICE_ROLE_KEY 单值变量并会报"Missing environment variable"错误,这说明新版多 key 机制(SECRET_KEYS/PUBLISHABLE_KEYS)正是为了取代旧变量而引入的——新代码应直接使用前者。
准则 10:设置其他自定义密钥(secrets)
除上述内置变量外的自定义环境变量,通过 env 文件批量注入:
supabase secrets set --env-file path/to/env-file
仓库示例同样印证了 env-file 工作流,例如 elevenlabs-speech-to-text 等函数 依赖 ELEVENLABS_API_KEY 一类自定义密钥;location 示例使用 IPINFO_TOKEN,同样需要开发者自行通过 supabase secrets set 配置后才能在 index.ts 中通过 Deno.env.get('IPINFO_TOKEN') 读取。本地开发时这类变量通常放在 .env 文件中,部署到远端则用上述命令上传。
准则 11:单函数多路由——建议用 Hono 或 Express
一个 Edge Function 可以承载多个路由。对于需要维护的复杂函数,建议引入路由库(如 Hono 或 Express),更便于开发者理解与维护。有两个关键点:
- 每个路由必须以
/function-name前缀开头,确保请求被正确路由到对应函数(Edge Functions 的请求路径与函数名强绑定); - 若使用 Hono 并需要按路由做 Supabase 鉴权,使用
npm:@supabase/server@^1/adapters/hono提供的适配器。
用 Express 组织函数内路由
import express from 'npm:express@^5'
const app = express()
app.get(/(.*)/, (req, res) => {
res.send('Welcome to Supabase')
})
app.listen(8000)
Express 示例在 app.listen(8000) 处监听端口——这正是 Supabase Edge Functions 提供给函数体的内部端口约定,外部流量由平台网关转发进来。同理,使用 node:http 的 createServer 自建 HTTP 服务时也应监听该固定端口(见"示例函数:使用 Node 内置 API")。仓库中 restful-tasks 即展示了在单个函数内组织多个 RESTful 路由的完整结构。
准则 12:文件写入只允许在 /tmp 目录
Edge Functions 是无状态的,运行时文件系统基本不可写,唯一的例外是 /tmp 临时目录。所有文件写入操作(无论用 Deno 还是 Node 的 File API)都只能在 /tmp 下进行,且数据不会在多次调用间持久化。涉及图像处理、PDF 生成、缓存下载内容等场景(例如仓库里的 image-manipulation 与 puppeteer 函数)都必须遵守此约束,需要持久化时应写入 Supabase Storage。
准则 13:用 EdgeRuntime.waitUntil 执行后台长任务
在函数响应返回后仍需执行的长任务(如清理、异步通知、埋点上报),应使用静态方法:
EdgeRuntime.waitUntil(promise)
它能让你把耗时的后台任务挂到请求生命周期之外,不阻塞当前响应。但不要假设它在所有请求/执行上下文中都可用——在调用前需要做能力探测或把调用收敛到统一封装中,以保证在纯 Deno 或其它运行时下的兼容回退。
可直接复用的函数模板
原文档提供了 5 个开箱即用的模板,下面逐一给出完整实现与要点注释。
模板 1(推荐):基于 withSupabase 的 CRUD 函数
import { withSupabase } from 'npm:@supabase/server@^1'
export default {
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
const { data, error } = await ctx.supabase.from('countries').select('*')
if (error) throw error
return Response.json({ data })
}),
}
这是社区推荐的标准形态:鉴权、RLS 作用域客户端、CORS 全部由封装器完成,函数体只需关注业务。部署时配合 verify_jwt = true(默认值)即可。仓库中的 location 以及 select-from-table-with-auth-rls 都是这一形态的实例。
模板 2:简单 Hello World
interface reqPayload {
name: string
}
console.info('server started')
export default {
fetch: async (req: Request) => {
const { name }: reqPayload = await req.json()
const data = {
message: `Hello ${name} from foo!`,
}
return Response.json(data)
},
}
注意模板中 interface reqPayload 的命名略不规范,实际项目中建议改写为 Payload 或 RequestBody。console.info('server started') 会在函数冷启动时输出一次,可用于确认部署成功。
模板 3:使用 Node 内置 API 的函数
import { randomBytes } from 'node:crypto'
import { createServer } from 'node:http'
import process from 'node:process'
const generateRandomString = (length) => {
const buffer = randomBytes(length)
return buffer.toString('hex')
}
const randomString = generateRandomString(10)
console.log(randomString)
const server = createServer((req, res) => {
const message = `Hello`
res.end(message)
})
server.listen(9999)
展示 node: 前缀导入与 Node HTTP 服务的写法。需要提醒的是,server.listen(9999) 是非标准端口,实际部署时应监听平台约定的内部端口(参见准则 11 中 Express listen(8000) 的说明),并优先考虑直接使用 export default { fetch } 契约而非自建服务。类型标注上建议补全:(req, res) 与 (length) 都未声明类型,生产代码应显式标注为 Request/ServerResponse 等 Node 类型(通过 npm:@types/node 引入)。
模板 4:在函数中使用 npm 包
import express from 'npm:express@^5'
const app = express()
app.get(/(.*)/, (req, res) => {
res.send('Welcome to Supabase')
})
app.listen(8000)
结合准则 3、4、5 理解:npm:express@^5 同时满足"前缀化"与"版本锁定"两条要求。app.get(/(.*)/, ...) 使用正则捕获全部 GET 路径,是兜底路由的常见写法。
模板 5:用内置 @Supabase.ai API 生成向量嵌入
const model = new Supabase.ai.Session('gte-small')
export default {
fetch: async (req: Request) => {
const params = new URL(req.url).searchParams
const input = params.get('text')
const output = await model.run(input, { mean_pool: true, normalize: true })
return Response.json(output)
},
}
Supabase.ai.Session('gte-small') 在函数运行时直接初始化嵌入模型会话;model.run(input, { mean_pool, normalize }) 完成推理,mean_pool: true 表示对 token 向量取均值池化、normalize: true 表示对结果做 L2 归一化,二者配合可让余弦相似度直接作为向量检索的评分。该能力适合在不额外调用外部 AI 服务的情况下快速产出向量——这也正是本仓库 向量搜索迁移 方向背后的基础设施之一。
在仓库中继续深挖的路径指引
- 官方示例库:examples/edge-functions/supabase/functions 下含 40+ 个真实函数,覆盖 RLS 查询、webhook、CORS、Stripe、Telegram/Discord Bot、图像处理、MCP 等场景;
- 各函数的 JWT 配置策略:examples/edge-functions/supabase/config.toml;
- 本仓库自身在用的边缘函数:supabase/functions(文档搜索嵌入与 OG 图片生成);
- 多函数共享依赖的组织方式:
supabase/functions/_shared目录与 import_map.json。
小结:写 Edge Functions 的最终检查清单
动笔(或让 AI 代笔)生成函数代码前,对照这 13 条逐一检查:
- ✅ 优先 Web API / Deno 核心 API,避免多余依赖;
- ✅ 复用代码收进
_shared并相对路径导入,函数间零耦合; - ✅ 外部依赖必须带
npm:/jsr:前缀; - ✅ 所有外部导入锁定版本号;
- ✅ 用
npm:/jsr:替换deno.land/x、esm.sh、unpkg.com直链; - ✅ Node 内置模块用
node:前缀导入; - ✅ 采用
export default { fetch }处理器契约,不用Deno.serve或旧版 stdserve; - ✅ 用
withSupabase声明auth模式,非'user'模式同步在config.toml设verify_jwt = false; - ✅ 优先使用预填的内置环境变量,不手工拼读 secret key;
- ✅ 自定义密钥用
supabase secrets set --env-file管理; - ✅ 多路由函数用 Hono/Express,路由加
/function-name前缀,Hono 鉴权走@supabase/server适配器; - ✅ 任何文件写入只允许落在
/tmp; - ✅ 长任务用
EdgeRuntime.waitUntil且做好可用性降级。
遵循这套准则产出的函数,天然具备可移植性、最小依赖面与内建安全边界,既能在本地 supabase start 中无缝调试,也能平滑部署到托管环境。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00