ADR 0009 — Eventos hacia afuera: CloudEvents, outbox, webhooks y brokers
- Estado: aceptada
- Fecha: 2026-09-25
Decisiones
- Formato estándar. Todo evento que sale del proceso es un CloudEvent 1.0 JSON con la extensión
tenantid. Elid(ULID) permite a los consumidores ser idempotentes.parseCloudEventvalida lo que llega. tenancy.events.forward(patrón, { transport }). Sin outbox, el envío es directo, fuera de la petición, con 3 intentos; el fallo final queda comoevents.transportbajo el tenant.'*'excluyetenancy.initialized/ended(uno por petición).- Outbox (
@tenancy-node/outbox). Si está instalada,forwardguarda el evento (una fila por destino) antes de que la operación continúe: si la outbox no responde, la operación falla en vez de perder el evento en silencio. El relay reserva lotes conFOR UPDATE SKIP LOCKEDy un lease (locked_until): varios relays trabajan en paralelo y si uno muere otro retoma su lote. Reintentos con espera exponencial; al agotarlos, dead-letter (status = failed) con reenvío manual. Garantía: al menos una vez. - Webhooks (
@tenancy-node/transport-webhook). Son un transporte más (webhooks): crean una entrega por endpoint que coincide, única por(endpoint, evento), así un evento reenviado no duplica entregas. Firmat=<unix>,v1=<HMAC-SHA256(t.body)>con tolerancia contra repeticiones y comparación en tiempo constante. Reintentos 1 min / 5 min / 30 min / 2 h / 12 h, circuit breaker por endpoint (abierto → pausa → half_open), dead-letter y reenvío. Protección SSRF: solo http(s), sin credenciales en la URL, sin redirecciones y ningún destino que resuelva a IPs internas o de metadata. - Brokers. RabbitMQ (exchange topic durable, mensajes persistentes, publisher confirms) y Redis Streams (
XADDconMAXLEN ~, grupos de consumidores,XAUTOCLAIM). Cada paquete trae un consumidor con confirmación después de procesar. - Eventos propios validados.
tenancy.events.define(tipo, esquemaValibot): publicar datos inválidos lanzaTENANCY_INVALID_EVENT_DATA.
Límites conocidos
- La outbox vive en la base central. Para los eventos del paquete (creación de tenants, etc.) el evento se guarda en la misma operación, pero no en la misma transacción SQL que el cambio; y los cambios de datos en la base de un tenant no pueden compartir transacción con la central. Una outbox por base de tenant queda como mejora futura.
- La protección SSRF resuelve el DNS antes de cada envío; un ataque de DNS rebinding entre esa resolución y la conexión sigue siendo posible. Para máxima seguridad, enviar los webhooks por un proxy de salida.
- Los endpoints reciben los eventos que se entregan después de registrarse (la coincidencia se decide al entregar).