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

tenantId contra sourceTenantId

notification-stack
último cambio 2026-09-25

Cada trabajo lleva dos identificadores de tenant y confundirlos manda un email con la marca equivocada. La distinción está en el contrato de cola:

Campo Qué es Para qué se usa
tenantId El tenant de entrega de notification-api Credenciales del proveedor, remitente y plantilla por defecto
sourceTenantId La clínica real a la que pertenece el destinatario Plantilla propia de la clínica si la tiene, analítica, cupo de rate-limit de la ingestión y purga

Por qué no son el mismo

Porque los tenants de notification-api no son las clínicas. Este stack es infraestructura compartida: lo usan PetID y template-api, y un tenant suyo representa un producto que envía notificaciones, no un negocio del producto.

En PetID hay un único tenant de entrega, el que en el despliegue se llama petid-system. Todas las clínicas envían a través de él, con su remitente y sus credenciales. El NOTIFICATIONS_TENANT_ID del .env de petid-api es ese UUID, y está fijo en el YAML del workflow.

Así que:

La plantilla: la de la clínica, con respaldo

La plantilla es lo único del envío que puede venir de la clínica. TemplateService.resolveWithFallback busca el code y el canal en sourceTenantId y en tenantId con una sola consulta (tenantId IN (clínica, entrega)): gana la de la clínica y, si no tiene, se usa la del tenant de entrega. Si ninguno la tiene, el mismo 404 de siempre. Sin sourceTenantId, o si es igual a tenantId, solo se mira el tenant de entrega.

Qué pasa si los intercambias

Poner el id de una clínica en tenantId no produce un error claro: produce un 404 de plantilla, porque el respaldo se busca en tenantId, que ahora es la propia clínica, y las clínicas normalmente no tienen plantillas. Si tuviera la suya, saldría con el remitente y las credenciales de la clínica en vez de los del tenant de entrega. Si alguna vez existiera un tenant de entrega por clínica, sería peor: se enviaría con la configuración de otro.

Al revés —poner el tenant de entrega en sourceTenantId— no rompe nada y es más difícil de detectar: todo llega bien y las estadísticas por clínica quedan mal, cosa que no se nota hasta que alguien mira una gráfica meses después.

El segundo uso: el cupo de envíos

notification-api limita las peticiones de ingestión por tenant, y el cupo se keyea por sourceTenantId, no por tenantId. La razón es justo lo que cuenta esta página: el tenant de entrega es constante para todo un producto, así que keyear por él mete a todas las clínicas —y a los dos canales— en un mismo cubo compartido, que es lo contrario del aislamiento que un límite por tenant pretende dar.

sourceTenantId ?? tenantId ?? ip

La cadena de respaldo existe porque sourceTenantId es opcional: un productor que no lo mande cae al tenant de entrega, y una ruta sin tenant en el cuerpo cae a la IP.

El límite de las dos rutas de encolado es de 300 peticiones por minuto por clínica, con @Throttle sobre NotificationsController. Es deliberadamente más alto que el global de 20/min del ThrottlerModule, que está pensado para la superficie CRUD interactiva: aquí quien llama es otro servicio detrás de ApiKeyGuard, y los productores envían a ráfagas — el cron de recordatorios de PetID encola una petición por cita en un bucle cerrado.

Esto se descubrió por las malas. Con el cupo keyeado por el tenant de entrega y puesto en 20/min, ese cron agotaba el presupuesto en segundos y todo lo que venía después recibía un 429 que el productor se tragaba: recordatorios perdidos sin error visible en ninguna parte.

No confundirlo con el límite de Resend. Son dos cosas en extremos opuestos del recorrido: este es de entrada, por clínica, y decide qué peticiones acepta notification-api; el de Resend es de salida, por cuenta, y afecta al throughput real de envío — ver credenciales por tenant.

Detalles que conviene recordar