tenantId contra sourceTenantId
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:
- Todas las notificaciones de PetID salen con
tenantId= el depetid-system. sourceTenantIdes lo único que dice de qué clínica salió cada una.
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.
- No consulta la tabla
Tenantpara la clínica: una clínica inactiva sigue usando su plantilla, y unsourceTenantIdque no existe en notification-api cae a la del tenant de entrega sin error. - Remitente y credenciales no cambian: salen siempre de
tenantId. Por eso una plantilla EMAIL de clínica tiene que apuntar a untemplateIdde la misma cuenta de Resend que el tenant de entrega; si no existe allí, esos envíos acaban enFAILEDy no hay respaldo. - Cuando la plantilla usada es la de la clínica, el trabajo lleva
templateTenantIdy el worker lo guarda enNotificationLog.meta(ver la vida de un log). - Lo resuelve la API al encolar. El worker no resuelve plantillas: recibe
templateIdotitle/bodyya elegidos.
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
sourceTenantIdes opcional (sourceTenantId?) y admite nulo en el log. Las filas anteriores a que existiera el campo lo tienen a null, así que cualquier agregado por clínica tiene que contemplarlo.- Del envío solo elige la plantilla, y con respaldo a la del tenant de entrega. No interviene en
el proveedor, las credenciales ni el remitente: eso es siempre
tenantId. Además decide, desde que existe el throttle por clínica, si la petición se acepta — ver arriba. - La purga de una clínica (
POST /tenants/:id/purge) borra los logs con esesourceTenantId, además de lo que el tenant tenga comotenantId(plantillas, credenciales, logs y el propio tenant). - La misma pareja aparece en la tabla
NotificationLog, con el mismo significado.