Email transaccional multi-tenant: dominios, marca y links de acción sin duplicar servicios
El email transaccional parece una pieza pequeña hasta que tienes más de un producto.
Con un solo producto, la configuración suele ser global:
from = Producto <no-reply@mail.producto.example>
appUrl = https://app.producto.example
tenantId = producto-slugPero en cuanto el mismo sistema de auth crea usuarios para otro producto, esa configuración global empieza a fallar.
El caso concreto: Ultimate Porra.
Si invito a alguien a una porra del Mundial y recibe un email del producto equivocado, con un link que acaba en un portal compartido en vez del producto que espera, el flujo puede ser técnicamente válido y aun así estar mal. El usuario no sabe dónde está, no reconoce la marca y pierde confianza en el enlace.
ImportantEn email transaccional, la marca no es decoración. Es parte de la seguridad percibida del flujo.
El problema real
El flujo inicial tenía este riesgo:
El bug no estaba en Resend ni en la plantilla. Estaba en el contrato entre productores y servicio de notificaciones.
El servicio de notificaciones ya podía enviar por tenant. Pero el servicio de auth necesitaba decir qué tenant correspondía a cada acción.
La arquitectura correcta
La separación que quiero es esta:
Cada pieza tiene una responsabilidad:
| Pieza | Responsabilidad |
|---|---|
| Servicio de auth | Saber para qué producto se crea la acción |
| Token de acción | Guardar metadata de producto para password setup/recovery |
| Servicio de notificaciones | Renderizar y encolar según tenant |
| Resend API key | Autorizar envío para un dominio |
| DNS del dominio | Probar SPF, DKIM y DMARC |
appUrl |
Llevar al usuario al producto correcto |
La clave es no duplicar el servicio entero por producto si sólo cambia la marca.
El contrato mínimo por producto
Cada producto necesita una entrada de configuración explícita:
{
"tenantId": "producto-slug",
"productCode": "producto-slug",
"brandName": "Nombre del producto",
"fromAddress": "Nombre del producto <no-reply@mail.producto.example>",
"replyTo": "soporte@producto.example",
"appUrl": "https://producto.example",
"authActionBaseUrl": "https://auth.example",
"sendingKeySecretRef": "clave de envio acotada al dominio"
}El detalle importante: authActionBaseUrl puede seguir siendo compartido.
No necesito un servicio de auth por producto si la página de acción sabe qué producto representa el token.
Tokens de acción con metadata
El flujo bueno queda así:
accion de crear usuario
productCode = producto-slug
servicio de auth
-> crea usuario pending
-> crea token password_setup
-> guarda metadata:
tenantId = producto-slug
productCode = producto-slug
brandName = Nombre del producto
appUrl = URL del producto
-> encola email en el servicio de notificaciones con el tenant del productoDespués, cuando el usuario abre el link:
lectura del token de accion
-> lee metadata del token
-> renderiza página con marca del producto
accion de completar
-> consume token
-> activa password
-> devuelve la URL de redirect del productoEsto resuelve dos problemas a la vez:
- el email sale desde el tenant correcto;
- la página de password y el redirect final pertenecen al producto correcto.
Por qué no basta con cambiar el remitente
Cambiar sólo fromAddress deja el flujo inconsistente.
| Elemento | Si queda global | Qué rompe |
|---|---|---|
fromAddress |
Notipad | El usuario no reconoce la invitación |
| plantilla | Notipad | La promesa de producto es incorrecta |
| action page | Notipad | El usuario cree estar creando otra cuenta |
| redirect final | portal compartido | El usuario aterriza fuera de la app |
| support URL | Notipad | Soporte y confianza quedan mezclados |
Email multi-tenant no significa "varios remitentes". Significa un flujo completo por producto.
Dominios de envío
Para producción, prefiero un subdominio de envío por producto:
mail.producto-a.example
mail.producto-b.exampleEso permite aislar reputación, intención y DNS.
La verificación mínima:
| Registro | Para qué sirve |
|---|---|
| SPF | Autoriza servidores a enviar por el dominio |
| DKIM | Firma el email y prueba que no fue alterado |
| DMARC | Define política y reporting si SPF/DKIM fallan |
Resend recomienda verificar dominios propios y usar subdominios de envío para aislar la reputación. Una vez SPF y DKIM están verificados, DMARC añade otra capa de confianza.
Free vs Pro no es sólo volumen
Para desarrollo o una beta muy pequeña, Free puede bastar.
Pero cuando hay dos productos reales, usuarios reales y emails de password/recovery, el coste de que el email no salga o llegue con límites incómodos pesa más que el precio del plan.
Mi regla:
Free -> desarrollo, pruebas, beta cerrada muy pequeña
Pro -> producción con usuarios reales y más de un dominio/productoNo es porque Pro sea mágicamente más seguro. Es porque producción necesita menos fricción: más margen mensual, sin límite diario del plan Free y con espacio para crecer sin cambiar arquitectura en mitad de un lanzamiento.
La cifra exacta puede cambiar; por eso la decisión operativa debe revisar la página de pricing antes de comprar.
n8n y la cola de emails
Si notifications-api trabaja en modo cola, n8n puede actuar como worker simple:
La ventaja es separar la experiencia de usuario del envío:
- crear usuario no depende de esperar a Resend;
- los fallos se reintentan;
- los jobs pueden auditarse;
- los tokens internos no llegan al browser.
La regla: n8n puede operar jobs, pero no debe conocer más secretos de los necesarios.
Checklist para añadir un producto nuevo
Checklist multi-tenant de email
- Crear subdominio de envío:
mail.<producto>.com. - Verificar dominio en Resend.
- Añadir SPF y DKIM en Cloudflare DNS.
- Añadir DMARC, aunque sea con política gradual.
- Crear una API key de envío acotada al dominio.
- Guardar la key en el almacén de secretos server-side.
- Añadir la configuración del tenant en ajustes server-side.
- Añadir el producto a la configuración permitida de acciones de auth.
- Hacer que el productor envíe
productCodeotenantId. - Probar
auth_password_setup. - Probar
password_recovery. - Confirmar que el link de password muestra la marca correcta.
- Confirmar que
completeredirige alappUrlcorrecto. - Revisar logs de cola y estado en Resend.
El patrón que queda
No quiero un servicio de email por producto.
Quiero un servicio compartido con límites claros:
producto selecciona tenant
tenant selecciona dominio, marca y provider key
token conserva contexto
email y redirect mantienen continuidadEso mantiene la arquitectura pequeña sin hacerla confusa.
La señal de que el diseño funciona es sencilla: un usuario de Ultimate Porra nunca tiene que preguntarse por qué Notipad le está pidiendo una contraseña.
Fuentes técnicas
- Resend documenta que la verificación de dominio requiere SPF y DKIM, y recomienda subdominios para aislar reputación e intención de envío: Managing Domains.
- Resend permite crear API keys con
Sending accessy restringirlas a un dominio concreto: API Keys. - Resend explica DMARC como una capa sobre SPF y DKIM para proteger contra spoofing y mejorar confianza: Implementing DMARC.
- Resend mantiene la información de pricing actualizada en su documentación y remite a la página pública de precios para cifras vigentes: What is Resend Pricing y Pricing.
Este artículo forma parte de mis notas de arquitectura y operación. Si te interesa el tema, sigue el blog o escríbeme.