Crear y Deployar Edge Functions
En esta guía vas a crear una Edge Function desde cero, entender su estructura, manejar CORS y secretos, y deployarla a producción. Al final vas a tener un webhook handler funcional.
Crear una función
Usa el CLI para generar el scaffold:
supabase functions new procesar-webhookEsto crea el archivo supabase/functions/procesar-webhook/index.ts:
// supabase/functions/procesar-webhook/index.ts
Deno.serve(async (req: Request) => {
const { name } = await req.json()
const data = {
message: `Hello ${name}!`,
}
return new Response(
JSON.stringify(data),
{ headers: { 'Content-Type': 'application/json' } },
)
})Estructura de una Edge Function
Toda Edge Function sigue el mismo patrón: recibir un Request, procesarlo y devolver un Response.
Acceder al body del request
Deno.serve(async (req: Request) => {
// Para JSON
const body = await req.json()
console.log(body.nombre, body.email)
// Para FormData
const formData = await req.formData()
const archivo = formData.get('archivo')
// Para texto plano
const texto = await req.text()
return new Response('OK')
})Acceder a headers y método
Deno.serve(async (req: Request) => {
const method = req.method // GET, POST, PUT, DELETE
const authHeader = req.headers.get('Authorization')
const contentType = req.headers.get('Content-Type')
const url = new URL(req.url)
const queryParam = url.searchParams.get('filtro')
if (method !== 'POST') {
return new Response('Metodo no permitido', { status: 405 })
}
// Procesar...
return new Response('OK')
})Devolver respuestas
Deno.serve(async (req: Request) => {
// Respuesta JSON
return new Response(
JSON.stringify({ resultado: 'exito', datos: [1, 2, 3] }),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
)
// Respuesta de error
return new Response(
JSON.stringify({ error: 'No encontrado' }),
{
status: 404,
headers: { 'Content-Type': 'application/json' },
}
)
})Usar el cliente de Supabase dentro de funciones
Puedes conectarte a tu propia base de datos de Supabase desde una Edge Function. El import map ya incluye @supabase/supabase-js:
import { createClient } from '@supabase/supabase-js'
Deno.serve(async (req: Request) => {
// Crear cliente con la service_role key para acceso completo
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Consultar datos
const { data: usuarios, error } = await supabase
.from('usuarios')
.select('id, nombre, email')
.eq('activo', true)
if (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
)
}
return new Response(
JSON.stringify({ usuarios }),
{ headers: { 'Content-Type': 'application/json' } }
)
})Variables automáticas
SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY están disponibles automáticamente en todas las Edge Functions. No necesitas configurarlas manualmente. Accedes a ellas con Deno.env.get().
Cliente con el token del usuario
Si quieres respetar las políticas de RLS (para que la función actúe con los permisos del usuario que la invoca), usa el token del header Authorization:
import { createClient } from '@supabase/supabase-js'
Deno.serve(async (req: Request) => {
const authHeader = req.headers.get('Authorization')
if (!authHeader) {
return new Response(
JSON.stringify({ error: 'Token requerido' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
}
// Cliente que respeta RLS con el token del usuario
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{
global: {
headers: { Authorization: authHeader },
},
}
)
// Esta query respeta las politicas de RLS
const { data, error } = await supabase
.from('posts')
.select('*')
return new Response(
JSON.stringify({ data }),
{ headers: { 'Content-Type': 'application/json' } }
)
})Manejo de CORS
Si tu frontend llama a una Edge Function directamente desde el browser, necesitas manejar CORS (Cross-Origin Resource Sharing -- el mecanismo que permite o bloquea requests entre dominios diferentes).
La forma más limpia es crear un archivo compartido con los headers de CORS:
// supabase/functions/_shared/cors.ts
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}Luego usalo en tu función:
import { corsHeaders } from '../_shared/cors.ts'
Deno.serve(async (req: Request) => {
// Manejar preflight request (OPTIONS)
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
// Tu logica aca
const data = { mensaje: 'Funciona' }
return new Response(
JSON.stringify(data),
{
headers: {
...corsHeaders,
'Content-Type': 'application/json',
},
}
)
})Preflight obligatorio
El bloque que maneja OPTIONS es obligatorio. Sin el, el browser va a bloquear el request real porque el preflight falla. Este es el error más común cuando empiezas con Edge Functions.
Gestión de secretos
Para API keys y valores sensibles que tu función necesita, usa los secretos de Supabase.
Configurar secretos
supabase secrets set MI_API_KEY=sk-1234567890
supabase secrets set RESEND_KEY=re_abcdefghAcceder a secretos en tu función
Deno.serve(async (req: Request) => {
const apiKey = Deno.env.get('MI_API_KEY')
const resendKey = Deno.env.get('RESEND_KEY')
// Usar las keys para llamar APIs externas
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${resendKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'tu-app@tudominio.com',
to: 'usuario@email.com',
subject: 'Notificacion',
html: '<p>Tu pedido fue procesado</p>',
}),
})
return new Response(
JSON.stringify({ enviado: response.ok }),
{ headers: { 'Content-Type': 'application/json' } }
)
})Listar y eliminar secretos
# Ver todos los secretos configurados (solo nombres, no valores)
supabase secrets list
# Eliminar un secreto
supabase secrets unset MI_API_KEYSecretos locales
Para desarrollo local, crea un archivo supabase/.env.local con tus secretos en formato CLAVE=valor. El CLI los carga automáticamente cuando ejecutas supabase functions serve.
Testear localmente
Levanta tu función en modo desarrollo:
supabase functions serve procesar-webhook --env-file supabase/.env.localPrueba con curl:
curl -i --location --request POST \
'http://localhost:54321/functions/v1/procesar-webhook' \
--header 'Authorization: Bearer TU_ANON_KEY_LOCAL' \
--header 'Content-Type: application/json' \
--data '{"evento": "pago.completado", "monto": 150}'Los logs se muestran en la terminal donde corre supabase functions serve. Cualquier console.log() en tu función aparece ahí.
Deployar a producción
Cuando tu función está lista, haz deploy con un solo comando:
supabase functions deploy procesar-webhookEsto sube tu función a la infraestructura de Supabase. La URL de producción es:
https://<tu-proyecto>.supabase.co/functions/v1/procesar-webhookDeploy de todas las funciones
supabase functions deploySin nombre específico, deploya todas las funciones en supabase/functions/.
No-verify JWT
Por defecto, las Edge Functions requieren un JWT (JSON Web Token) válido en el header Authorization. Si tu función es un webhook público que recibe requests de un servicio externo, deploya con --no-verify-jwt:
supabase functions deploy procesar-webhook --no-verify-jwtEjemplo completo: Webhook handler
Este es un ejemplo real de una Edge Function que recibe webhooks de un servicio de pagos, valida la firma, actualiza la base de datos y envía una notificación:
// supabase/functions/webhook-pagos/index.ts
import { createClient } from '@supabase/supabase-js'
import { corsHeaders } from '../_shared/cors.ts'
interface WebhookPayload {
evento: string
pedido_id: string
monto: number
moneda: string
cliente_email: string
}
Deno.serve(async (req: Request) => {
// Manejar CORS preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
// Solo aceptar POST
if (req.method !== 'POST') {
return new Response(
JSON.stringify({ error: 'Metodo no permitido' }),
{ status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
try {
// Validar la firma del webhook
const firma = req.headers.get('x-webhook-signature')
const secreto = Deno.env.get('WEBHOOK_SECRET')
if (!firma || firma !== secreto) {
return new Response(
JSON.stringify({ error: 'Firma inválida' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
// Parsear el payload
const payload: WebhookPayload = await req.json()
// Crear cliente de Supabase con acceso completo
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Actualizar el estado del pedido en la base de datos
if (payload.evento === 'pago.completado') {
const { error: updateError } = await supabase
.from('pedidos')
.update({
estado: 'pagado',
monto_pagado: payload.monto,
pagado_en: new Date().toISOString(),
})
.eq('id', payload.pedido_id)
if (updateError) {
console.error('Error al actualizar pedido:', updateError.message)
return new Response(
JSON.stringify({ error: 'Error al procesar' }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
// Registrar el evento en una tabla de logs
await supabase.from('webhook_logs').insert({
evento: payload.evento,
pedido_id: payload.pedido_id,
payload: payload,
procesado: true,
})
console.log(`Pedido ${payload.pedido_id} marcado como pagado`)
}
return new Response(
JSON.stringify({ recibido: true }),
{
status: 200,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
}
)
} catch (err) {
console.error('Error en webhook:', err)
return new Response(
JSON.stringify({ error: 'Error interno' }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
})Para deployar este webhook:
# Configurar el secreto
supabase secrets set WEBHOOK_SECRET=mi-secreto-del-servicio-de-pagos
# Deploy sin verificación de JWT (es un webhook externo)
supabase functions deploy webhook-pagos --no-verify-jwtProblemas comunes
"Boot failure" al deployar
Revisa que no estés importando módulos de Node.js incompatibles con Deno. Usa npm: como prefijo si necesitas un paquete de npm.
"Invalid JWT" al invocar
La función está verificando JWT pero no estás mandando el header Authorization. Si es un webhook público, re-deploya con --no-verify-jwt.
CORS bloqueado en el browser
Asegúrate de que tu función maneje el request OPTIONS y devuelva los headers de CORS. Es el error más frecuente.
Timeout
Las Edge Functions tienen un timeout de 150 segundos (2.5 minutos). Si tu función necesita más tiempo, tienes que dividir el trabajo en pasos más pequeños o usar una cola de tareas.
Resumen
| Tarea | Comando / Concepto |
|---|---|
| Crear función | supabase functions new nombre |
| Estructura | Deno.serve((req) => Response) |
| Cliente Supabase | createClient() con env vars automáticas |
| CORS | Archivo _shared/cors.ts + manejar OPTIONS |
| Secretos | supabase secrets set CLAVE=valor |
| Test local | supabase functions serve nombre |
| Deploy | supabase functions deploy nombre |
| Webhook público | Deploy con --no-verify-jwt |