Supabase Edge Functions: El Pattern que el 95% de Developers No Implementa Correctamente
Aprende el pattern correcto para Supabase Edge Functions en producción: orquestación atómica, gestión de secrets, testing local y CI/CD con GitHub Actions.
La Mayoría Usa Supabase Edge Functions Como Wrappers de Base de Datos
Tienes una Edge Function que recibe un request, hace una query, devuelve datos.
Eso no es una Edge Function bien construida. Es una API de los 90 con latencia de Deno.
*El problema real no es Supabase. Es que no entiendes lo que las Edge Functions realmente son.*
No son endpoints REST glorificados. Son unidades de lógica de negocio que viven en el edge, tienen acceso directo a tu base de datos, y pueden orquestar operaciones complejas sin exponer tu schema.
Este artículo muestra el pattern que separa los proyectos que escalan de los que explotan en producción.
El Error Arquitectónico Más Común en Edge Functions
La mayoría construye así:
❌ Approach incorrecto:
```typescript
// supabase/functions/get-user/index.ts
serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')! // ERROR CRÍTICO
)
const { data } = await supabase
.from('users')
.select('*') // Exponiendo todo el schema
.eq('id', userId)
return new Response(JSON.stringify(data))
})
```
Tres problemas en diez líneas:
→ Usa `ANON_KEY` en lugar de `SERVICE_ROLE_KEY` para operaciones de servidor
→ Hace `select('*')` exponiendo columnas que no deberías devolver
→ Zero validación de entrada ni manejo de errores
✅ Approach correcto:
```typescript
// supabase/functions/get-user/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { z } from 'https://deno.land/x/zod/mod.ts'
const RequestSchema = z.object({
userId: z.string().uuid()
})
serve(async (req) => {
try {
// Autenticación del caller primero
const authHeader = req.headers.get('Authorization')
if (!authHeader) {
return new Response('Unauthorized', { status: 401 })
}
// Cliente con service role para operaciones de servidor
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Validación con Zod antes de tocar la DB
const body = await req.json()
const { userId } = RequestSchema.parse(body)
// Select explícito — nunca select('*')
const { data, error } = await supabase
.from('users')
.select('id, email, display_name, created_at')
.eq('id', userId)
.single()
if (error) throw error
return new Response(
JSON.stringify({ user: data }),
{ headers: { 'Content-Type': 'application/json' } }
)
} catch (err) {
const status = err.name === 'ZodError' ? 400 : 500
return new Response(
JSON.stringify({ error: err.message }),
{ status, headers: { 'Content-Type': 'application/json' } }
)
}
})
```
Validación, autenticación, select explícito, manejo de errores tipado.
Eso es una Edge Function de producción.
El Pattern de Orquestación que Nadie Enseña
El verdadero poder de las Edge Functions no está en los endpoints simples.
Está en la orquestación de operaciones atómicas que necesitan garantías transaccionales.
Ejemplo real: un usuario completa una compra. Necesitas:
→ Crear el registro de orden en la DB
→ Reducir el stock del producto
→ Enviar email de confirmación
→ Disparar un webhook a un sistema externo
Si haces esto desde el cliente en cuatro llamadas separadas, tienes condiciones de carrera garantizadas.
✅ Pattern de orquestación atómica:
```typescript
// supabase/functions/process-order/index.ts
serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
const { productId, quantity, userId } = await req.json()
// Ejecutar toda la lógica en una RPC de PostgreSQL
// para garantías transaccionales reales
const { data: order, error } = await supabase
.rpc('create_order_atomic', {
p_product_id: productId,
p_quantity: quantity,
p_user_id: userId
})
if (error) {
return new Response(
JSON.stringify({ error: 'Order failed', detail: error.message }),
{ status: 422 }
)
}
// Side effects DESPUÉS de confirmar la transacción
// Estos pueden fallar sin afectar la orden
await Promise.allSettled([
fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: 'orders@tuapp.com',
to: order.user_email,
subject: `Orden #${order.id} confirmada`
})
}),
notifyWebhook(order)
])
return new Response(JSON.stringify({ order }), { status: 201 })
})
```
*El patrón clave: transacciones en PostgreSQL via RPC, side effects con `Promise.allSettled` fuera de la transacción.*
Si el email falla, la orden no se deshace. Si la orden falla, no envías emails fantasma.
Secrets y Variables de Entorno: El Setup que el Tutorial Oficial Omite
El mayor vector de vulnerabilidad en Edge Functions no es el código.
Es la gestión de secrets.
❌ Lo que hace la mayoría:
```bash
Hardcodean valores en el código
const apiKey = 'sk-live-abc123...' // Esto va a tu repositorio
```
✅ El setup correcto con Supabase CLI:
```bash
Paso 1: Define todos tus secrets localmente
supabase secrets set RESEND_API_KEY=tu_valor
supabase secrets set STRIPE_WEBHOOK_SECRET=tu_valor
supabase secrets set INTERNAL_API_KEY=tu_valor
Paso 2: Verifica qué secrets tienes configurados
supabase secrets list
Paso 3: Para CI/CD, usa el flag --env-file
supabase functions deploy process-order \
--project-ref tu_project_ref
```
Nunca un secret en el código. Nunca en un `.env` commiteado.
Siempre via `supabase secrets set` para producción.
El Patrón de Testing que Hace Deployables las Edge Functions
El mayor obstáculo para tener Edge Functions en producción real no es escribirlas.
Es testearlas localmente antes del deploy.
Supabase tiene un emulador local que la mayoría ignora completamente.
```bash
Arranca el stack completo localmente
supabase start
En otra terminal, ejecuta tu función
supabase functions serve process-order --env-file .env.local
Testea con curl
curl -i --location --request POST \
'http://localhost:54321/functions/v1/process-order' \
--header 'Authorization: Bearer TU_ANON_KEY_LOCAL' \
--header 'Content-Type: application/json' \
--data '{"productId": "uuid-aqui", "quantity": 2, "userId": "uuid-aqui"}'
```
Este setup te da:
→ Base de datos PostgreSQL local real
→ Auth emulado
→ Storage emulado
→ Edge Functions ejecutándose en Deno real
No hay excusa para hacer deploy sin haber testeado localmente primero.
Cuándo NO Usar Edge Functions
El error real no es implementarlas mal. Es usarlas cuando no deberías.
No uses Edge Functions para:
→ Operaciones de solo lectura simples que Row Level Security (RLS) ya cubre
→ Transformaciones de datos que puedes hacer en el cliente
→ Lógica que cambia cada semana — las Edge Functions tienen friction de deploy
→ Operaciones que necesitan más de 150ms de CPU — el límite de Deno en edge es real
Sí usa Edge Functions para:
→ Lógica que no puede vivir en el cliente por seguridad
→ Orquestación de múltiples servicios externos (Resend, Stripe, webhooks)
→ Operaciones que necesitan `SERVICE_ROLE_KEY` — nunca en el cliente
→ Procesamiento de webhooks entrantes con verificación de firma
→ Scheduled jobs via pg_cron + HTTP calls
*La regla real no es "usa Edge Functions para todo". Es "usa Edge Functions cuando el cliente no puede hacer esto de forma segura".*
El Deploy Pattern para Producción
Nunca hagas deploy manual en producción.
Este es el workflow con GitHub Actions:
```yaml
.github/workflows/deploy-functions.yml
name: Deploy Edge Functions
on:
push:
branches: [main]
paths: ['supabase/functions/**']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
uses: actions/checkout@v3
uses: supabase/setup-cli@v1
with:
version: latest
name: Deploy functions
run: |
supabase functions deploy process-order \
--project-ref ${{ secrets.SUPABASE_PROJECT_REF }}
supabase functions deploy get-user \
--project-ref ${{ secrets.SUPABASE_PROJECT_REF }}
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
```
Solo se dispara cuando cambias archivos en `supabase/functions/`. Sin cambios irrelevantes que generen deploy innecesarios.
Takeaways
→ Usa siempre `SERVICE_ROLE_KEY` en Edge Functions de servidor — nunca `ANON_KEY`
→ Valida con Zod antes de tocar la DB — zero confianza en inputs externos
→ RPC para operaciones atómicas — las transacciones viven en PostgreSQL, no en tu función
→ `Promise.allSettled` para side effects — emails y webhooks no deben bloquear ni revertir la operación principal
→ Gestiona secrets con Supabase CLI — nunca hardcodeados, nunca en `.env` commiteado
→ Testea localmente con `supabase start` — el emulador es production-grade
→ CI/CD con GitHub Actions — deploy manual en producción es un antipatrón
Las Edge Functions de Supabase son la pieza que convierte un proyecto de side project en arquitectura de producción real.
El developer que las domina construye backends completos sin gestionar infraestructura.
Lee el artículo completo en brianmenagomez.com

