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:

bash
supabase functions new procesar-webhook

Esto crea el archivo supabase/functions/procesar-webhook/index.ts:

typescript
// 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

typescript
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

typescript
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

typescript
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:

typescript
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:

typescript
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:

typescript
// 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:

typescript
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

bash
supabase secrets set MI_API_KEY=sk-1234567890
supabase secrets set RESEND_KEY=re_abcdefgh

Acceder a secretos en tu función

typescript
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

bash
# Ver todos los secretos configurados (solo nombres, no valores)
supabase secrets list
 
# Eliminar un secreto
supabase secrets unset MI_API_KEY
Secretos 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:

bash
supabase functions serve procesar-webhook --env-file supabase/.env.local

Prueba con curl:

bash
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:

bash
supabase functions deploy procesar-webhook

Esto sube tu función a la infraestructura de Supabase. La URL de producción es:

plaintext
https://<tu-proyecto>.supabase.co/functions/v1/procesar-webhook

Deploy de todas las funciones

bash
supabase functions deploy

Sin 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:

bash
supabase functions deploy procesar-webhook --no-verify-jwt

Ejemplo 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:

typescript
// 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:

bash
# 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-jwt

Problemas 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

TareaComando / Concepto
Crear funciónsupabase functions new nombre
EstructuraDeno.serve((req) => Response)
Cliente SupabasecreateClient() con env vars automáticas
CORSArchivo _shared/cors.ts + manejar OPTIONS
Secretossupabase secrets set CLAVE=valor
Test localsupabase functions serve nombre
Deploysupabase functions deploy nombre
Webhook públicoDeploy con --no-verify-jwt