Estándares de Edge Functions
Estándares de calidad para Edge Functions de Supabase o Lovable Cloud (Deno runtime).
Aplican igual si el código lo escribe una persona o una IA (Lovable u otra).
Objetivo: máxima velocidad con cambios pequeños y consistentes, sin sacrificar seguridad ni mantenibilidad.
Glosario
- RLS (Row-Level Security): Seguridad a nivel de fila en Postgres. Permite definir políticas para que cada usuario solo pueda leer/escribir las filas que le corresponden (por ejemplo,
user_id = auth.uid()), incluso si intenta hacer queries directas fuera del frontend. - PII (Personally Identifiable Information): Información Personal Identificable. Datos que pueden identificar a una persona directa o indirectamente (ej. nombre, email, teléfono, documento, dirección). Requiere mayor cuidado: mínimo acceso, retención definida y protección con RLS cuando aplique.
- CORS (Cross-Origin Resource Sharing): Reglas del navegador que controlan si un sitio web puede llamar a tu API/Edge Function desde otro dominio. Por eso manejamos el “preflight” (
OPTIONS) y devolvemos headers de CORS para permitir/denegar el acceso. - Preflight: Petición previa que hace el navegador (normalmente
OPTIONS) para preguntar si tiene permiso de hacer la petición real (por ejemplo, si puede usarPOSTy enviarAuthorization). Si el preflight falla, el navegador bloquea la llamada. - Request: La “petición” que llega a tu función (método como
GET/POST, headers, URL y body). Es lo que tu endpoint recibe y procesa. - Parsear (parse): Convertir el body de la request (por ejemplo JSON en texto) a un objeto que el código pueda usar. Si el JSON está mal formado, el parseo falla y debe responderse con error de request inválida.
- Service role: Tipo de usuario con permisos elevados (administrador) en Supabase. Puede ejecutar operaciones saltándose RLS. Se usa solo para tareas internas/admin y siempre con autorización explícita.
- Testing: Pruebas automatizadas que verifican que el código hace lo esperado. Nos sirven para detectar errores rápido, evitar regresiones y mantener velocidad sin romper cosas.
Alcance
Incluye:
- Estructura de funciones, flujo HTTP, CORS, auth/authz, validación.
- Manejo de errores y responses consistentes.
- Uso de
_shared/, env/secrets, logging. - Testing mínimo y baseline de seguridad.
Reglas no negociables
- Runtime es Deno (NO Node): no usar APIs de Node, no
process.env. - Nunca duplicar helpers: todo lo común se importa desde
_shared/. - Nunca usar
new Response()directo: usarsuccessResponse/errorResponse(incluyen CORS). - Nunca
console.log: usarlogger. - Nunca
Deno.env.get(): usar helpersgetEnv*. - Errores de negocio: siempre
AppError+ErrorCodes(no string literals). - Delete por defecto: soft delete (
deleted_at/is_deleted). Hard delete requiere justificación + documentación. - Admin client (service role) bypassea RLS: usar solo con autorización explícita + logging.
Estructura de carpetas y archivos
- Cada función vive en:
supabase/functions/<function-name>/(kebab-case) - Obligatorio:
index.ts(entry point conDeno.serve) - Opcional (solo si ayuda a mantener
index.tspequeño):service.ts(lógica de negocio)schema.ts(tipos/validación)handlers.ts(si hay routing interno)README.md(si hay endpoint público, hard delete, webhooks, etc.)
- Prohibido: crear carpetas
utils/,helpers/,lib/dentro de funciones (va a_shared/).
Flujo estándar de request (STRICT)
Orden recomendado en todas las funciones:
- CORS: manejar el
OPTIONSy responder de inmediato si es un preflight. - Autenticación: leer el token del request (header
Authorization). - Preparar clientes: crear los clientes de Supabase que usará la función (usuario/admin según aplique).
- Confirmar usuario: validar que el token sea válido y obtener el usuario (salvo que el endpoint sea público y esté documentado).
- Leer el body: si el request trae JSON, parsearlo.
- Validar datos: verificar campos requeridos y reglas básicas (formatos, límites, etc.).
- Ejecutar la lógica: hacer la operación real (consultar/crear/actualizar).
- Responder: devolver una respuesta exitosa con el formato estándar.
- Manejar errores: si algo falla, registrar el error y devolver una respuesta de error estándar.
Responses y contratos
Para que el frontend y el equipo puedan trabajar rápido sin adivinar, todas las funciones deben responder con un formato consistente:
- Respuesta exitosa: usar el helper
successResponse(...)- Puede incluir solo los datos, o datos + un mensaje.
- Respuesta con error: usar el helper
errorResponse(...)- Incluye un mensaje claro y un código de error consistente.
Reglas:
- No construir respuestas “a mano” (no devolver objetos distintos en cada endpoint).
- No enviar detalles internos al cliente (por ejemplo, SQL, mensajes crudos del servidor o trazas de error).
Códigos de respuesta (baseline)
200/201: todo salió bien (éxito).400: el request viene mal (faltan campos, formato inválido).401: no hay sesión/token válido (no está autenticado).403: está autenticado, pero no tiene permiso.404: el recurso no existe.409: conflicto (por ejemplo, ya existe / ya fue procesado / idempotencia).500: error interno (mensaje genérico para el cliente).
Manejo de errores (STRICT)
La idea es que los errores se manejen siempre igual, para que sea fácil debuggear y para que el cliente reciba respuestas claras.
- Todo lo que hace la función debe estar dentro de un bloque que permita capturar errores (try/catch).
- Cuando ocurra un error “esperado” (por ejemplo: falta un campo, no tiene permiso, no se encontró algo), debemos devolver un error controlado usando nuestro formato estándar (AppError + ErrorCodes).
- Si ocurre un error “inesperado” (algo que no anticipamos), devolvemos un mensaje genérico al cliente y lo investigamos con logs.
Reglas:
- Antes de responder con error, siempre registrar el error con
logger.error(...), incluyendo contexto útil (qué operación se intentó, IDs relevantes, requestId, etc.). - Nunca exponer al cliente detalles internos del sistema (trazas, SQL, info sensible).
Authentication vs Authorization
Autenticación (Authentication / AuthN) responde a:
- “¿Quién eres?”
- Ejemplo: validar que el token/sesión sea real y obtener el usuario.
Autorización (Authorization / AuthZ) responde a:
- “¿Puedes hacer esto?”
- Ejemplo: aunque el usuario sea válido, confirmar si puede ver/editar ese recurso específico.
Importante: que alguien esté autenticado no significa que tenga permiso para cualquier acción.
Reglas
- Por defecto, todos los endpoints son privados: requieren usuario autenticado.
- Un endpoint público es una excepción y:
- debe quedar documentado en un
README.mdde esa función, - debe validar el input con más rigor (para evitar abuso: payloads grandes, spam, etc.).
- debe quedar documentado en un
Autorización (permisos) en operaciones con datos
Cada vez que un endpoint lee, modifica o borra información, debe existir una regla clara que garantice que el usuario tiene permiso. Se puede cumplir de estas formas:
- Dueño del recurso: el usuario solo puede operar sobre registros que le pertenecen (por ejemplo, “solo tus notas / tus órdenes”).
- RLS en la base de datos: dejar que la base de datos aplique la regla (si está bien configurada), usando el cliente “normal” (anon) para respetar esas políticas.
- Service role (admin): si la acción es administrativa (ver/modificar data de otros usuarios), primero se valida que el usuario tenga ese rol/permiso y solo entonces se usa el cliente admin.
Regla práctica:
- Si para “que funcione” estás usando el cliente admin sin validar permisos, el endpoint está inseguro.
Supabase clients (anon vs admin)
En Edge Functions usamos dos formas de conectarnos a Supabase, y la diferencia es crítica para la seguridad:
-
Cliente normal (
anonClient)- Se comporta como un usuario normal.
- Respeta las reglas de acceso (RLS) definidas en la base de datos.
- Es el default para endpoints “user-facing” (cuando el usuario está operando sobre sus propios datos).
-
Cliente administrador (
adminClient)- Tiene permisos elevados.
- Puede saltarse las reglas de acceso (RLS).
- Solo se usa en casos específicos, por ejemplo:
- tareas internas del sistema / administración,
- consultas que involucran data de múltiples usuarios (y están justificadas),
- procesos por lotes controlados (batch jobs),
- casos puntuales donde RLS no cubre lo necesario, pero con permisos validados antes.
Regla clave:
Si estás usando adminClient solo para “que funcione”, eso es una señal de problema:
- Faltan políticas RLS,
- Falta validar permisos (autorización) antes de acceder/modificar datos.
Parsing y validación
Antes de ejecutar cualquier lógica, debemos asegurarnos de que los datos que llegan al endpoint son válidos y seguros.
-
Si el request trae un body en JSON:
- Primero leerlo y convertirlo a objeto (si el JSON viene roto, se responde con error).
- Luego verificar que estén los campos obligatorios.
-
Validaciones comunes (según el caso):
- emails con formato correcto (y normalizados para evitar duplicados por mayúsculas/espacios),
- acciones/valores permitidos (por ejemplo, solo aceptar
create | update | delete, no cualquier string).
Reglas:
- Si llegan campos extra o inesperados y eso puede causar problemas, rechazar el request (especialmente en endpoints públicos).
- Poner límites para evitar abuso:
- Tamaño máximo de texto,
- Cantidad máxima de elementos en arrays,
- Tamaño máximo del body.
Esto evita que alguien mande requests gigantes o maliciosos que ralenticen o tumben el servicio.
Operaciones de base de datos (desde Edge)
Soft delete (default)
Cuando “borramos” algo, no lo eliminamos de verdad. Lo que hacemos es marcarlo como eliminado:
- En vez de hacer
DELETE, hacemos un UPDATE para marcar el registro:deleted_at = now()
Regla:
- Todas las consultas normales (las que usan los usuarios) deben ignorar los registros marcados como eliminados.
- Es decir: solo mostrar lo que no esté “borrado”.
Esto nos permite:
- Recuperar datos si fue un error,
- Auditar cambios,
- Evitar pérdidas irreversibles.
Hard delete (excepción)
Eliminar un registro de forma permanente (hard delete) es una excepción. Solo se permite cuando:
- Hay una obligación legal (por ejemplo, eliminación definitiva por privacidad),
- Es data temporal/efímera (ej. OTP),
- Existe un requerimiento explícito aprobado.
Si se hace hard delete, es obligatorio:
- Documentar el motivo en el
README.mdde esa función, - Registrar el evento en logs (qué se borró y por qué), para auditoría.
Logging (structured)
Los logs son nuestra principal herramienta para entender qué pasó en producción sin perder tiempo.
Reglas:
- Usar siempre el logger del proyecto (
logger.debug/info/warn/error) en lugar deconsole.log. - Mantener un formato consistente (logs “estructurados”): además del mensaje, incluir datos útiles como metadata.
Mínimo recomendado:
- Al iniciar una petición: registrar un log tipo “Start” con:
- nombre de la función,
requestId,userIdsi existe.
- Si algo falla: registrar un log tipo “Failure” con:
requestId,userIdsi existe,- el error,
- y contexto (qué operación se intentaba).
Seguridad en logs (no negociable):
- Nunca loggear información sensible:
- tokens, header
Authorization, - contraseñas,
- API keys,
- PII (datos personales) sensibles.
- tokens, header
Siempre incluir contexto útil:
requestId(para rastrear la petición),userId(si aplica),- acción/operación,
- IDs relevantes (por ejemplo
orderId,workspaceId, etc.).
Env vars y secrets
Regla general: los secretos y configuraciones no se escriben en el código.
- Leer variables de entorno solo con los helpers del proyecto:
getEnv,getEnvNumber,getEnvBoolean,getEnvJson
- Nunca hardcodear secretos en el repo.
- Nunca devolver secretos en respuestas ni incluirlos en mensajes de error.
Local vs producción:
.envse usa solo para desarrollo local dentro desupabase/functions/y debe estar ignorado por git.
Convenciones:
- Variables custom en
UPPER_SNAKE_CASE. - Usar prefijo por servicio cuando aplique (ej.
STRIPE_SECRET_KEY). - Si una función depende de variables específicas, documentarlo en su
README.md.
Naming conventions (resumen)
- Carpetas:
kebab-case(process-payment,get-user-profile) - Archivos:
index.ts,service.ts,schema.ts,handlers.ts,README.md(lowercase) - Variables Funciones:
camelCase - Typos o Classes:
PascalCase - Constantes:
UPPER_SNAKE_CASE - Booleanos:
is/has/should - Códigos de error:
UPPER_SNAKE_CASE(desdeErrorCodes)
Testing standards (mínimo)
Qué testear:
_shared/siempre (unit tests).- lógica compleja en
service.ts(unit tests). - flows críticos (authentication/authorization, errores, DB writes) deberían tener tests de función (cuando estén disponibles en el setup).
Cobertura (Objetivos):
_shared: 95%+- lógica crítica: 90%+
- paths de error: obligatorios
Baseline de seguridad
Antes de dar por “lista” una Edge Function, revisa este checklist. Es el mínimo para evitar incidentes y retrabajo.
Checklist mental en cada función:
- Autenticación activa por defecto (si es público, debe estar documentado).
- Permisos claros: verificar que el usuario realmente puede hacer esa acción (por ownership, RLS o rol admin).
- Inputs validados: no confiar en datos del cliente; validar campos, formatos y límites.
- Secretos bien usados: leer env vars solo con
getEnv*y no exponerlos. - Logs seguros: no registrar tokens,
Authorization, API keys ni datos personales. - Cliente admin con cuidado: usar
adminClientsolo con autorización explícita y logging. - CORS correcto: manejar preflight (
OPTIONS) para que el navegador no bloquee la llamada. - Borrado lógico: por defecto usar soft delete, no hard delete.
Checklist de calidad (Edge Functions)
A) Estructura
- Función creada en
supabase/functions/<kebab-case>/index.ts - No se crearon helpers locales (todo shared está en
../_shared/) -
index.tsidealmente < 200 líneas (si crece, extraer aservice.ts)
B) Imports y runtime
- No hay imports de Node / no se usan APIs de Node
- No hay
Deno.env.get()niprocess.env - No hay
console.log
C) CORS
-
handleCorsPreflight(req)es lo primero y respondeOPTIONS
D) Authentication/Authorization
- Endpoint autentica por defecto (
getAuthenticatedUser) - Si es público: está documentado en
README.mdy tiene validaciones/limitaciones - Para writes/lecturas sensibles: ownership o autorización explícita
- No se usa
adminClientsin authorization explícita + logging
E) Parsing y validación
- Body parseado con
parseJsonBody<T> - Campos requeridos con
validateRequired - Validadores específicos aplicados (email/action/limits)
F) Errores y responses
- Todo envuelto en
try/catch - Errores de negocio usan
AppError+ErrorCodes -
successResponse/errorResponse(sinnew Response) - Errores loggeados con
logger.error+ contexto
G) DB operations
- Se usa
anonClientpor defecto (RLS) - Soft delete implementado para borrados
- Hard delete solo si aplica +
README.md+logger.warncon reason
H) Env/secrets
- Env vars via
getEnv* - No secrets hardcodeados
- No secrets en logs ni en responses
- Variables custom documentadas (si aplica)
I) Tests (cuando aplica)
-
_shared/cubierto con unit tests -
service.tscon tests si hay lógica crítica - Casos mínimos para críticos: 400/401-403/200 (+ 409 si idempotencia)