ecosystem-wikicómo extender los templates sin romper sus convenciones
‹ todo el contenido

Qué hace el stack y por qué son dos repos

notification-stack
último cambio 2026-09-25

El stack de notificaciones son dos servicios que no se hablan por HTTP. Entre ellos hay una cola de BullMQ sobre Redis, y esa separación es lo que explica casi todo lo demás.

petid-api / template-api
        ↓ HTTP + API key
notification-api        valida, resuelve plantilla y tenant, encola
        ↓ BullMQ (Redis)
notification-worker     resuelve credenciales, entrega, registra
        ↓
   Resend / FCM

Quién hace qué

notification-api es la cara pública. Recibe “manda el aviso X al destinatario Y”, y antes de aceptar nada comprueba que el tenant existe y que hay una plantilla registrada para ese código y ese canal (la de la clínica de sourceTenantId si la tiene; si no, la del tenant de entrega). Si algo falla, falla en la petición de quien llamó. También es dueña del CRUD de tenants, plantillas y logs.

notification-worker no expone HTTP —ni siquiera un healthcheck—. Consume de las dos colas, resuelve las credenciales del proveedor, entrega y escribe el resultado.

Por qué están separados

Porque fallan de formas distintas y se recuperan de formas distintas. Una plantilla mal configurada es un error del llamante y debe devolver un 400 al momento. Que Resend esté caído no es culpa de nadie y lo que toca es reintentar más tarde. Metidos en un mismo proceso, la petición HTTP tendría que esperar a la entrega o mentir diciendo que todo fue bien.

La consecuencia práctica está escrita en EmailService: la validación se hace al encolar, a propósito.

Comprobarlo aquí hace que una plantilla mal configurada falle en la petición del llamante en vez de fallar tres veces dentro del worker después de que el trabajo ya fue aceptado.

Tres veces porque los trabajos se encolan con attempts: 3 y backoff: 5000.

Dos colas, pero un solo proceso

La separación por canal llega hasta la cola y no más allá. app.module.ts registra las dos (registerQueue({ name: QUEUES.EMAIL }, { name: QUEUES.PUSH })), cada processor lleva su decorador —@Processor(QUEUES.EMAIL) y @Processor(QUEUES.PUSH)— pero los dos viven en el mismo proceso, y el compose levanta un único contenedor notification-worker, sin réplicas.

Conviene tenerlo claro porque invita a una conclusión falsa. Que haya dos colas aísla lo que se espera de una cola: cada canal tiene su orden y su política de reintentos, y un atasco de push no bloquea la cola de email. Lo que no aísla son los recursos: comparten event loop, y si el contenedor se cae o se redespliega, los dos canales paran a la vez.

Es lo correcto para el volumen actual, y separar despliegues por canal es el paso siguiente cuando uno de los dos lo justifique — a cambio de más piezas que desplegar.

Ojo con un detalle no escrito en ningún sitio hasta ahora: la concurrencia es 1, el valor por defecto de BullMQ, porque nadie la configuró. Cada processor coge un trabajo cada vez, y eso es hoy el único freno del lado del worker contra el límite de peticiones del proveedor. Subirla para acelerar una cola es quitar ese freno sin darse cuenta: si algún día se sube, el limiter de la cola se pone en el mismo commit.

Es transporte, no orquestador

El stack entrega lo que le mandan, por donde le dicen. No decide por qué canal conviene avisar a alguien, y no sabe qué prefiere recibir cada usuario: quien llama elige canal, destinatario y plantilla, y el stack lo transporta.

Eso se descartó a propósito. “Avisa del evento X por el canal que prefiera el usuario” obligaría al stack a conocer usuarios y preferencias de cada producto, y dejaría de ser compartible entre productos que modelan al usuario de formas distintas.

La consecuencia más visible es que los device tokens los aporta el productor. El endpoint de push recibe devices: [{ token, platform? }], no un userId que el stack tendría que resolver — igual que en email recibe la dirección y no busca a quién pertenece. platform (ios, android o web) es opcional y solo se registra en el log; el envío va siempre por FCM. La forma anterior, deviceTokens (una lista de strings), se sigue aceptando como obsoleta mientras haya productores que la usen (template-api): la API deja un warn deprecated deviceTokens y rechaza la petición que mande las dos a la vez. Guardarlos, renovarlos y limpiarlos es trabajo de quien llama; el stack solo le devuelve la señal de cuáles están muertos, ver tokens de push muertos.

Si algún día la gestión de tokens se duplicase de forma dolorosa entre productos, ese es el momento de reabrirlo — no antes.

No es infraestructura de PetID

Aunque hoy se note sobre todo por PetID, este stack es compartido: template-api lo consume igual, con el mismo cliente HTTP portado. Al cambiar algo aquí, el radio de impacto incluye productos que no aparecen en este wiki.

Por eso el tenantId que maneja no es una clínica, sino un tenant de entrega — ver tenantId contra sourceTenantId.

Y un tercer repo que no es un servicio

notification-stack no ejecuta nada: contiene el docker-compose, los dos Dockerfiles y el deploy.sh que levantan a los otros dos. Cuando una página de este wiki describe la frontera entre api y worker en lugar de un lado, se archiva aquí.