Cambiar tema

Supabase Edge Functions en la práctica: runtime Deno y guía de desarrollo con TypeScript

Easton editorial illustration: hardened server operations console

El teléfono vibra sin parar. Los webhooks de Stripe devuelven 500 en producción: el pago se completó, pero el pedido no se creó.

Revisando los logs, el culpable era la antigua función serverless: el arranque en frío era demasiado largo y Stripe agotaba el tiempo de espera antes de recibir respuesta. Y además había que montar un gateway API para validar firmas y gestionar CORS…

Esa noche empecé a estudiar en serio Supabase Edge Functions. Al principio, «runtime Deno» me frenó un poco — años escribiendo en Node.js, cambiar de runtime implica aprender APIs nuevas. Pero en la práctica la lógica es distinta: no se trata de «migrar», sino de una opción más ligera para escenarios que no requieren dependencias pesadas.

Este artículo comparte las trampas que encontré y lo que aprendí: arquitectura de Edge Functions, diferencias entre Deno y Node.js, flujo de desarrollo y depuración local, y cómo escribir una API de forma elegante con Hono.

Qué son Edge Functions — arquitectura y elección tecnológica

Empecemos por qué es Edge Functions y por qué Supabase eligió Deno en lugar de Node.js.

Ejecución en el borde, no hosting centralizado en la nube

Edge Functions son funciones TypeScript que se ejecutan en nodos edge. A diferencia de Lambda o Vercel Functions, no se despliegan en unas pocas grandes regiones: se distribuyen en cientos de puntos edge en todo el mundo.

¿Qué implica? Un usuario en Shanghái puede ejecutar la función en Tokio: la latencia baja de cientos de milisegundos a unas decenas.

Pero el borde tiene un coste: la función no puede ser demasiado pesada. Cada función corre en un isolate V8, con su propio heap de memoria y hilo de ejecución; arranque en milisegundos, pero memoria y tiempo de ejecución limitados. Ideal para operaciones de corta duración: webhooks, imágenes OG, llamadas a APIs de terceros, envío de correo.

Menos adecuado: tareas de larga duración, bibliotecas Node.js nativas pesadas, acceso al sistema de archivos.

Por qué Deno

Pasé mucho tiempo en las GitHub Discussions de Supabase. La explicación oficial, en resumen:

  1. Arranque rápido: Deno empaqueta el código en formato ESZip; arranque en frío de 0-5 ms. Lambda con Node.js suele tardar 100-500 ms.
  2. Modelo de seguridad: acceso a sistema de archivos y red deshabilitados por defecto; autorización explícita. Importante en entornos edge multiinquilino — no quieres que la función de otro lea tus datos.
  3. TypeScript nativo: sin tsconfig ni ts-node; un archivo .ts y listo. Para quien ya hace backend en TypeScript, menos configuración.
  4. Portabilidad: Deno se integra en otras aplicaciones. Supabase usa su propio fork deno_core, optimizado para escenarios embebidos.

A cambio, el ecosistema es más pequeño que el de Node.js; algunos paquetes npm no funcionan. Deno ahora admite npm specifiers (import { xxx } from 'npm:lodash'), la compatibilidad ha mejorado.

Panorama de la arquitectura

Flujo aproximado de una solicitud:

Cliente → CDN/gateway edge → validación JWT → isolate V8 ejecuta la función → respuesta

Punto clave: la validación JWT — Edge Functions verifica por defecto el header Authorization. Para acceso público, despliega con --no-verify-jwt.


Configuración del entorno de desarrollo y comandos CLI

Conceptos claros; pasemos a la práctica.

Instalar Supabase CLI

En macOS, con Homebrew:

brew install supabase/tap/supabase

Linux y Windows tienen métodos equivalentes; la documentación oficial es clara, no los repito.

Luego inicia sesión:

supabase login

Se abre el navegador para autorizar el CLI en tu cuenta de Supabase.

Inicializar el proyecto

En el directorio del proyecto:

supabase init

Crea el directorio supabase/ con config.toml y el subdirectorio functions/ si no existe.

Crear la primera Edge Function

supabase functions new hello-world

El comando crea supabase/functions/hello-world/ con un archivo 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',
      'Connection': 'keep-alive',
    },
  })
})

Así de simple. Deno.serve() es la API nativa de Deno; Request y Response son Web API estándar, como fetch en el navegador.

Servidor de desarrollo local

supabase functions serve --env-file supabase/.env.local

Inicia un servidor local en http://localhost:54321; la función está en http://localhost:54321/functions/v1/hello-world.

La primera trampa que encontré: olvidar iniciar la pila local de Supabase (PostgreSQL incluido). Orden correcto:

# Primero la pila local
supabase start

# Luego las funciones
supabase functions serve

Solicitud de prueba

Con curl o HTTPie:

curl -i --location --request POST 'http://localhost:54321/functions/v1/hello-world' \
  --header 'Authorization: Bearer <your-anon-key>' \
  --header 'Content-Type: application/json' \
  --data '{"name":"World"}'

Respuesta:

{
  "message": "Hello World!"
}

Funciona.

La recarga en caliente es automática: modifica el código y guarda, sin reiniciar el servicio.

Variables de entorno

No pongas secretos en el código. Supabase gestiona variables mediante archivos .env:

# Crear archivo .env
echo "MY_SECRET=super_secret_value" > supabase/.env.local

# Leer en la función
const mySecret = Deno.env.get('MY_SECRET')

En producción, con supabase secrets set:

supabase secrets set MY_SECRET=super_secret_value

En la práctica: API RESTful con Hono

Deno.serve() basta para casos simples; con enrutamiento, middleware y validación, hacerlo a mano es penoso.

Ahí entra Hono.

Qué es Hono

Hono es un framework web ultraligero para runtimes edge. Deno, Cloudflare Workers, Bun… enrutamiento performante y TypeScript de primera.

Oficialmente «small, simple, and ultrafast» — mi experiencia lo confirma.

Integración en Edge Functions

Nueva función:

supabase functions new user-api

Luego modifica index.ts:

import { Hono } from 'jsr:@hono/hono'
import { cors } from 'jsr:@hono/hono/cors'
import { logger } from 'jsr:@hono/hono/logger'

const app = new Hono().basePath('/api')

// Middleware
app.use('*', cors())
app.use('*', logger())

// Rutas
app.get('/users/:id', (c) => {
  const id = c.req.param('id')
  return c.json({ user: { id, name: 'Demo User', email: '[email protected]' } })
})

app.post('/users', async (c) => {
  const body = await c.req.json<{ name: string; email: string }>()
  // Aquí puedes conectar la base de datos de Supabase
  return c.json({ created: body }, 201)
})

app.put('/users/:id', async (c) => {
  const id = c.req.param('id')
  const body = await c.req.json<{ name?: string; email?: string }>()
  return c.json({ updated: { id, ...body } })
})

app.delete('/users/:id', (c) => {
  const id = c.req.param('id')
  return c.json({ deleted: id })
})

// Inicio
Deno.serve(app.fetch)

Puntos importantes:

  1. jsr:@hono/hono es el formato JSR de Deno, no npm. JSR es el registro oficial de Deno.
  2. basePath('/api') prefija las rutas con /api.
  3. c es el context de Hono: solicitud, respuesta y utilidades.
  4. c.json() establece Content-Type y gestiona null/undefined.

Conexión a la base de datos de Supabase

Hono es el framework web; para la base de datos hace falta el cliente de Supabase. Ejemplo completo:

import { Hono } from 'jsr:@hono/hono'
import { createClient } from 'jsr:@supabase/supabase-js@2'

const app = new Hono().basePath('/api')

// Cliente Supabase
const supabaseUrl = Deno.env.get('SUPABASE_URL')!
const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!

const supabase = createClient(supabaseUrl, supabaseKey, {
  auth: {
    autoRefreshToken: false,
    persistSession: false,
  },
})

// GET /api/users — lista
app.get('/users', async (c) => {
  const { data, error } = await supabase
    .from('users')
    .select('id, name, email, created_at')

  if (error) {
    return c.json({ error: error.message }, 500)
  }
  return c.json({ users: data })
})

// POST /api/users — creación
app.post('/users', async (c) => {
  const body = await c.req.json<{ name: string; email: string }>()

  const { data, error } = await supabase
    .from('users')
    .insert(body)
    .select()
    .single()

  if (error) {
    return c.json({ error: error.message }, 400)
  }
  return c.json({ user: data }, 201)
})

Deno.serve(app.fetch)

Uso SUPABASE_SERVICE_ROLE_KEY: permisos completos, omite RLS. Gestiona con cautela en producción.

Manejo de errores y validación

Hono no trae validador integrado, pero Zod encaja bien:

import { z } from 'npm:zod'
import { zValidator } from 'jsr:@hono/zod-validator'

const userSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
})

app.post(
  '/users',
  zValidator('json', userSchema),
  async (c) => {
    const validated = c.req.valid('json')
    // validated ya es un objeto con tipos seguros
    return c.json({ received: validated })
  }
)

Si falla la validación: 400 con detalle de errores.


Despliegue y buenas prácticas en producción

Funciona en local; toca producción.

Comando de despliegue

supabase functions deploy user-api

En el primer despliegue el CLI pregunta qué proyecto de Supabase vincular. Después: subida, build y despliegue automáticos.

URL de la función:

https://[PROJECT_ID].supabase.co/functions/v1/user-api

Variables de entorno y Secrets

En producción hay que configurar las variables por separado:

supabase secrets set SUPABASE_URL=https://xxx.supabase.co
supabase secrets set SUPABASE_SERVICE_ROLE_KEY=eyJxxx...

Los Secrets se almacenan cifrados; lectura con Deno.env.get() en tiempo de ejecución.

Estrategia de validación JWT

Edge Functions valida JWT por defecto:

  • Solo pasan solicitudes con Authorization: Bearer <token> válido
  • La información del usuario del token está en los headers de req

Para API pública (webhooks de terceros), --no-verify-jwt:

supabase functions deploy user-api --no-verify-jwt

Cualquiera puede invocar la función — la validación en código te toca a ti.

Reducir la latencia del arranque en frío

Deno arranca rápido, pero se puede hacer más:

  1. Reducir el volumen de dependencias: prioriza paquetes Deno/JSR, menos npm
  2. Carga diferida: módulos grandes con import() bajo demanda
  3. Funciones ligeras: una función, una responsabilidad — no metas todo el backend dentro

Supabase recomienda menos de 2 segundos de ejecución y arranque en frío de 0-5 ms. Estos consejos aceleran la respuesta:

Monitorización y logs

El Dashboard muestra logs de invocaciones y errores. También puedes integrar Sentry u otros servicios.

La API EdgeRuntime.waitUntil() permite seguir en segundo plano tras la respuesta:

EdgeRuntime.waitUntil(
  fetch('https://analytics.example.com/track', { method: 'POST', body: '...' })
)

return new Response('OK')

El cliente recibe la respuesta sin esperar la tarea en segundo plano.


Conclusión

¿Para qué escenarios encaja Edge Functions?

Adecuado:

  • Webhooks (Stripe, GitHub, Slack)
  • Generación de imágenes OG
  • Inferencia de IA (llamadas a API LLM)
  • Correo y notificaciones
  • Procesamiento de datos de corta duración

Menos adecuado:

  • Tareas de larga duración (transcodificación de vídeo)
  • Bibliotecas Node.js nativas pesadas
  • Acceso al sistema de archivos

Si ya usas base de datos y autenticación de Supabase, Edge Functions extiende el stack de forma natural — sin montar servidores extra ni operaciones pesadas; escribes la lógica de negocio.

¿Frente a Cloudflare Workers o Vercel Functions? Cada uno tiene ventajas. Workers es más maduro; Vercel se integra con Next.js. Pero con Supabase, Edge Functions ofrece la mejor integración — cliente de base de datos, auth y almacenamiento listos.

Para empezar: github.com/supabase/supabase/tree/master/examples/edge-functions

Preguntas en los comentarios, o en el Discord de Supabase.

Flujo completo de desarrollo y despliegue de Supabase Edge Functions

Guía operativa completa, desde la configuración del entorno hasta el despliegue en producción

⏱️ Estimated time: 45 min

  1. 1

    Step 1: Instalar Supabase CLI e iniciar sesión

    Instala el CLI con Homebrew (macOS):

    ```bash
    brew install supabase/tap/supabase
    supabase login
    ```

    El inicio de sesión abre el navegador para autorizar el CLI en tu cuenta de Supabase.
  2. 2

    Step 2: Inicializar el proyecto y crear una función

    Ejecuta el comando de inicialización en el directorio del proyecto y crea tu primera función:

    ```bash
    supabase init
    supabase functions new hello-world
    ```

    Esto crea una plantilla de función en el directorio `supabase/functions/`.
  3. 3

    Step 3: Iniciar el entorno de desarrollo local

    Primero inicia la pila local de Supabase (PostgreSQL incluido) y luego el servicio de funciones:

    ```bash
    supabase start
    supabase functions serve --env-file supabase/.env.local
    ```

    Dirección local: `http://localhost:54321/functions/v1/{function-name}`
  4. 4

    Step 4: Construir una API con el framework Hono

    Instala Hono y crea una API RESTful:

    ```typescript
    import { Hono } from 'jsr:@hono/hono'
    import { cors } from 'jsr:@hono/hono/cors'

    const app = new Hono().basePath('/api')
    app.use('*', cors())
    app.get('/users/:id', (c) => {
    return c.json({ user: { id: c.req.param('id') } })
    })
    Deno.serve(app.fetch)
    ```

    Hono admite enrutamiento, middleware y validación de parámetros.
  5. 5

    Step 5: Configurar variables de entorno y Secrets

    Archivo `.env` en local, Secrets en producción:

    ```bash
    # Local
    echo "MY_SECRET=value" > supabase/.env.local

    # Producción
    supabase secrets set MY_SECRET=value
    ```

    Lectura en la función mediante `Deno.env.get('MY_SECRET')`.
  6. 6

    Step 6: Desplegar en producción

    Despliega la función y configura el acceso público si es necesario:

    ```bash
    # Despliegue estándar (requiere validación JWT)
    supabase functions deploy user-api

    # API pública (sin validación JWT)
    supabase functions deploy user-api --no-verify-jwt
    ```

    Formato de URL de producción: `https://[PROJECT_ID].supabase.co/functions/v1/user-api`

FAQ

¿Cuál es la diferencia entre Supabase Edge Functions y Cloudflare Workers?
Ambos son plataformas de computación en el borde, con algunas diferencias: (1) Edge Functions se integra profundamente con la base de datos, autenticación y almacenamiento de Supabase, listo para usar; (2) runtime Deno frente al motor V8 — Edge Functions admite npm specifiers y JSR; (3) arranque en frío a nivel de milisegundos, rendimiento comparable. Si ya usas Supabase, la experiencia de integración de Edge Functions es mejor.
¿Edge Functions es adecuado para procesar webhooks?
Muy adecuado. El webhook es un caso típico: (1) arranque en frío rápido — Stripe, GitHub, etc. no agotan el tiempo de espera; (2) puedes verificar la firma, procesar la lógica de negocio y escribir en la base de datos de Supabase; (3) despliega como API pública con --no-verify-jwt.
¿En qué se diferencia la gestión de paquetes de Deno respecto a Node.js?
Deno usa JSR (registro oficial de Deno) y npm specifiers: (1) paquetes JSR importados con formato `jsr:@hono/hono`; (2) paquetes npm con formato `npm:zod`; (3) sin package.json ni node_modules, dependencias gestionadas automáticamente. La mayoría de las bibliotecas habituales están soportadas.
¿Cómo conectar la base de datos de Supabase desde Edge Functions?
Usando el cliente @supabase/supabase-js: (1) importa `jsr:@supabase/supabase-js@2`; (2) lee SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY de las variables de entorno; (3) al crear el cliente, configura auth.autoRefreshToken: false (innecesario en el borde); (4) SERVICE_ROLE_KEY omite RLS — controla los permisos en producción.
¿Edge Functions tiene límite de tiempo de ejecución?
Sí. Supabase recomienda no superar los 2 segundos por función, arranque en frío de 0-5 ms. Adecuado para operaciones breves (webhooks, inferencia de IA, envío de correo), no para tareas largas (transcodificación de vídeo, procesamiento masivo de datos).
¿Cómo depurar Edge Functions?
Varias opciones: (1) `supabase functions serve` en local + recarga en caliente; (2) los `console.log()` aparecen en los logs del Dashboard; (3) `supabase functions serve --env-file` para cargar variables locales; (4) solicitudes de prueba con curl o Postman.

9 min de lectura · Publicado el: 19 abr 2026 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog