Supabase Edge Functions + Resend 实战:用 send-email-resend 在边缘发送事务邮件
导读
本文以仓库 examples/edge-functions 中的 send-email-resend 示例 为核心,讲解如何基于 Supabase Edge Functions(Deno 运行时)调用 Resend 邮件 API 发送事务邮件:从准备 API Key 与域名校验、配置本地 .env,到用 Supabase CLI 本地起服务、无 JWT 联调,最后部署到云端。读完你将掌握一个可运行的最小发信函数,并理解其 fetch 处理器的关键实现、config.toml 中 verify_jwt 的取舍逻辑,以及同一目录下向 SMTP、React Email 模板方案演进的方向。
示例定位:仓库中与发信相关的三个示例
在阅读具体实现前,先明确 send-email-resend 在仓库中的位置。整个示例集合位于 examples/edge-functions/supabase/functions,发信相关共有三个互相补充的目录:
- send-email-resend:本文主角,最简实现,直接以原生
fetch调用https://api.resend.com/emails,用于理解 Resend 发送 API 的最小协议; - send-email-smtp:基于
nodemailer走通用 SMTP 协议,适合有自建 SMTP/其他邮件服务商的场景; - auth-hook-react-email-resend:引入 Resend 官方 SDK(
resend@^6)与@react-email/render,配合 Supabase Auth 的 Send Email Hook,用 React 模板渲染 HTML 邮件。
本文聚焦第一个示例,但会在最后一节横向对比三者,方便你按需取舍。
前置条件
根据 send-email-resend/README.md 的 Prerequisites,运行该示例前需要完成三件事:
- 在 Resend 后台创建 API Key,用于后续的身份认证;
- 在 Resend 后台完成域名验证(Domain verification),保证发送身份可信(示例代码默认走
@resend.dev测试域名,实际生产务必换成已验证域名); - 准备本地
.env文件并写入RESEND_API_KEY。
仓库在 examples/edge-functions/supabase/.env.local.example 中为整个示例集合提供了统一的本地环境变量模板,其中与本文相关的配置段为:
# send-email-resend
RESEND_API_KEY=
创建本地环境文件的方式,与仓库根 README 推荐的一致:
cp .env.example .env
若按整个 edge-functions 仓库的开发流程走,则是:
cp ./supabase/.env.local.example ./supabase/.env.local
注意 .env 属于敏感文件,应加入 .gitignore,避免将密钥提交进仓库。除 RESEND_API_KEY 外,同一个模板文件里还列出了 SMTP_*、OPENAI_API_KEY、STRIPE_API_KEY 等其他示例共用的密钥,你只需关心与当前函数相关的条目。
源码拆解:一个 25 行的最小发信函数
整个函数只有一份实现文件 send-email-resend/index.ts,完整代码如下:
import { withSupabase } from 'npm:@supabase/server@^1'
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY')
// Accepts a signed-in user's JWT or a secret key, so deploy with verify_jwt = false.
export default {
fetch: withSupabase({ auth: ['user', 'secret'] }, async (req, ctx) => {
const res = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${RESEND_API_KEY}`,
},
body: JSON.stringify({
from: 'onboarding@resend.dev',
to: 'delivered@resend.dev',
subject: 'hello world',
html: '<strong>it works!</strong>',
}),
})
const data = await res.json()
return Response.json(data)
}),
}
逐行解读其中值得借鉴的关键点:
- 入口方式:Supabase Edge Functions 在 Deno 上运行,导出的是带
fetch方法的默认对象。withSupabase来自npm:@supabase/server@^1(本目录 README 之外仓库其他函数也大量使用),是 Supabase 提供的一个高层封装,用于把传入请求与 Supabase 上下文(auth、database client 等)绑定。这里通过auth: ['user', 'secret']显式声明:该函数同时接受已登录用户的 JWT或Service Role 这类 secret key。 - 密钥读取:
Deno.env.get('RESEND_API_KEY')在模块加载时读取环境变量。如果本地漏配.env.local或云端漏配 Secret,该值会是undefined,发出的请求将因缺少Bearer凭据被 Resend 拒绝——排查发信失败时,这是最先要检查的点。 - 对接 Resend HTTP API:使用标准
fetch向POST https://api.resend.com/emails发送 JSON,鉴权方式为请求头Authorization: Bearer <RESEND_API_KEY>。请求体中from/to两个字段指向 Resend 官方测试地址(onboarding@resend.dev、delivered@resend.dev),html字段承载邮件正文。 - 返回响应:
res.json()读取 Resend 返回的载荷(成功时含邮件id,失败时含错误信息),再经Response.json(data)以 JSON 形式回给调用方,便于本地 curl 直接核对结果。
与 send-email-smtp/index.ts 对比可明显看出两种集成路线的差异:SMTP 版通过 nodemailer.createTransport(...) 建立传输通道后用 transport.sendMail(...) 发送;而 Resend 版不依赖任何 SDK,一个 fetch 即完成投递,体积最小、依赖最少。
本地运行与联调
README 的 Instructions 给出了两条本地命令,前提是已安装最新版 Supabase CLI:
supabase start
supabase functions serve --no-verify-jwt --env-file ./supabase/.env.local
各命令作用如下:
supabase start:在本机(借助 Docker)拉起整套本地 Supabase 栈。本仓库对应的 config.toml 中配置了本地 API 端口54321、数据库端口54322等,启动后可提供本地函数网关。supabase functions serve --no-verify-jwt --env-file ./supabase/.env.local:启动本地函数开发服务器,--env-file指向我们上一步填好RESEND_API_KEY的.env.local;--no-verify-jwt表示跳过 JWT 校验,方便在浏览器或 curl 中直接调用调试。
服务起来后,直接在浏览器或 curl 访问 README 给出的本地端点即可触发发信:
GET http://localhost:54321/functions/v1/send-email-resend
在示例集合根 README 中可以看到,仓库推荐的本地开发流程与此完全一致:先 supabase start,再复制 .env.local 并填入对应密钥,然后 supabase functions serve --env-file ./supabase/.env.local --no-verify-jwt,最后用函数示例内的 CURL 命令或 Supabase 客户端库的 invoke 方法发起测试请求。这说明 send-email-resend 完全可以复用整个仓库的标准本地链路。
部署到 Supabase 云端
本地验证通过后,按 README 执行部署命令:
supabase functions deploy resend --no-verify-jwt
需要指出两处与函数实际命名的细节:
- 函数源码位于目录 functions/send-email-resend,而 README 的部署命令写作
deploy resend。以目录名send-email-resend部署(supabase functions deploy send-email-resend)与该函数实际的本地 URL 路径/functions/v1/send-email-resend保持一致,是更稳妥的做法;若使用--no-verify-jwt部署,建议显式声明该函数不校验 JWT,避免与前端场景错配。 - 云端密钥需要在部署前设置。仓库根 README 给出的标准做法是:
supabase secrets set --env-file ./supabase/.env.local
它会将 .env.local 中的全部变量(含 RESEND_API_KEY)同步为线上 Secret;也可用 supabase secrets list 验证是否写入成功。生产环境建议单独维护一个不含本地杂项变量的 .env 文件用于云端。
部署配置佐证:config.toml 中的 verify_jwt
为什么 README 反复强调 --no-verify-jwt?查看本仓库的 config.toml 可以找到一致性的配置证据。该文件为每个示例函数都声明了独立配置段,其中:
[functions.send-email-resend]
verify_jwt = false
即该函数在 Supabase Functions 平台上的默认 JWT 校验开关被显式关闭。这与源码注释 // Accepts a signed-in user's JWT or a secret key, so deploy with verify_jwt = false. 互为印证:withSupabase({ auth: ['user', 'secret'] }, ...) 已经自行完成了对「用户 JWT 或 secret key」的鉴权,若网关层再强制校验 JWT,会导致携带 Secret 的服务器端调用被拦在门外,因此必须 verify_jwt = false。同理,需要登录态才允许调用的 send-email-smtp 在 config.toml 中对应段则是 verify_jwt = true,两种配置形成鲜明对照,可作为你设计自己函数鉴权策略时的参考模板。
从测试域名走向生产:调用方鉴权与模板化进阶
作为演示,示例默认使用 Resend 的测试地址 onboarding@resend.dev / delivered@resend.dev。要真正对外发信,需满足两个前提:域名已通过 Resend 验证,且发件人替换为验证域名下的真实地址。
若你需要更完整的生产级发信链路,仓库内现成的同类示例可以直接迁移:
- 鉴权模式对齐:想让该函数接受「任意已登录用户」调用,可参考 send-email-smtp 把
withSupabase的鉴权收敛为{ auth: 'user' }(对应verify_jwt = true),并通过Authorization: Bearer <USER_ACCESS_TOKEN>头调用。 - HTML 邮件模板:参考 auth-hook-react-email-resend/index.ts,它使用
npm:resend@^6的new Resend(Deno.env.get('RESEND_API_KEY'))与resend.emails.send({ from, to, subject, html }),并用@react-email/render+react@^19把MagicLinkEmail、SignUpEmail这类 React 组件渲染成 HTML 正文;其部署同样配合verify_jwt = false(由 Supabase Auth 以 Webhook 签名方式调用)。如果你需要在注册、登录等 Auth 事件时发送漂亮的自定义邮件,这是比手动拼html字符串更可维护的升级路线。
小结
通过 send-email-resend 示例,一条完整的「Supabase Edge Function + Resend 发信」链路已经跑通:Deno.env 读密钥 → withSupabase 包裹处理器 → fetch 调 Resend API → Response.json 回包;配合 .env.local、supabase functions serve、supabase functions deploy 与 config.toml 的 verify_jwt 配置即可完成从本地到云端的部署。作为最薄的 Resend 集成样例,它天然适合作为你在 Supabase 平台上构建通知、验证码、事务邮件等能力的第一块积木。
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 StartedRust0623
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