一句行话:Edge Functions 是 Supabase 里的"无服务器函数",跑在 Deno 运行时上、部署到边缘节点。它补足了前面组件的空白——当逻辑既不是简单 CRUD、又不适合塞进数据库函数时,就放在这里。这一节我们讲它适合干什么、怎么写、怎么安全地用管理员密钥,以及和数据库函数的取舍。
适合:
不适合:
函数代码放在 supabase/functions/函数名/index.ts,用 Deno 的 serve 入口:
// supabase/functions/hello/index.ts import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' Deno.serve(async (req) => { const { name } = await req.json().catch(() => ({ name: 'world' })) const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')! ) return new Response(JSON.stringify({ message: `hello ${name}` }), { headers: { 'Content-Type': 'application/json' }, }) })
部署:
supabase functions deploy hello # 访问:https://<项目ref>.functions.supabase.co/hello
有些逻辑需要绕过 RLS(比如管理员给用户发通知)。这时在函数里用 SUPABASE_SERVICE_ROLE_KEY,但务必自己校验调用者是否真的有权,绝不能信任客户端声明的"我是管理员":
// supabase/functions/admin-notify/index.ts import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' Deno.serve(async (req) => { // 1. 用 anon 客户端解析调用者 JWT const authHeader = req.headers.get('Authorization')! const client = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')! ) const token = authHeader.replace('Bearer ', '') const { data: user } = await client.auth.getUser(token) if (!user.user) return new Response('未登录', { status: 401 }) // 2. 查数据库确认其是否为管理员(而非信请求体) const admin = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // 仅此处用,绕过 RLS ) const { data: role } = await admin .from('user_roles') .select('is_admin') .eq('user_id', user.user.id) .single() if (!role?.is_admin) return new Response('无权限', { status: 403 }) // 3. 真正的管理员操作... return new Response('已通知') })
关键点:管理员身份是从数据库查出来的,不是从请求参数读的。service_role 只在"已确认有权"之后、且只用于必要的管理员操作。
下面 SVG 画了"函数用双客户端做权限校验"的模式,避免 service_role 滥用:

背景:用支付平台,用户付款后平台回调一个 webhook,要把订单标为已支付并开通权限。
操作过程:
pay-webhook 函数,验证回调签名(防伪造),再用 admin 客户端写库:// supabase/functions/pay-webhook/index.ts import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' Deno.serve(async (req) => { const body = await req.text() // 校验签名(伪代码,按支付平台文档实现) if (!verifySignature(req.headers.get('x-signature')!, body)) return new Response('bad sig', { status: 400 }) const event = JSON.parse(body) const admin = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ) await admin .from('orders') .update({ paid: true }) .eq('id', event.order_id) return new Response('ok') })
https://<ref>.functions.supabase.co/pay-webhook。paid=true。结果:支付状态可靠落库,且因为只有带正确签名的请求才能改库,伪造回调无效。
解读:webhook 是 Edge Function 的典型主场——它要根据外部事件触发、要验签、要绕过 RLS 写库,又不适合放进数据库函数(签名算法在 SQL 里写很别扭)。
变式:如果担心函数执行超时,把"验签+记原始事件"先做,真正的"开通权限"用数据库触发器在 orders.paid 变 true 时异步完成,函数只负责收件,职责更单一。
⚠️ 函数里用了 service_role 就等于拿到数据库管理员权限。任何从 req 来的"角色/权限"字段都不可信,必须回数据库查。把校验写在前、管理员操作写在后,顺序不能反。
💡 我们建议给所有对外函数加签名校验或至少要求带有效 JWT,不要让函数成为"无需认证就能触发管理员操作"的后门。函数入口是第一道防线。
第三章我们把能跑的主题全走了一遍:建项目、CRUD、认证、实时、存储、函数。第四章进入生产级话题:性能、安全、部署、排障、演进。