Un email de onboarding parece una pieza pequeña del producto: alguien crea una cuenta, recibe un mensaje y empieza a usar la aplicación. En un SaaS que crece, sin embargo, ese mensaje depende de varias cosas: la transacción de registro, una cola, un proveedor de correo, una plantilla y un enlace correcto.
Cuando algo falla, el equipo suele preguntar “¿se envió el email?” Esa pregunta es demasiado corta. Un evento puede haberse creado, procesado y entregado, pero la persona no haberlo visto porque la plantilla tenía un enlace roto o porque el mensaje terminó en una bandeja equivocada.
La solución que me ha resultado más práctica es tratar cada email como un contrato de eventos. No hace falta construir una plataforma enorme. Hace falta definir qué ocurrió, qué evidencia guardamos y qué puede comprobar un test.
El problema: onboarding sin evidencia
En una primera versión, el backend normalmente devuelve 201 Created y deja el envío para un worker. Eso es correcto para la experiencia del usuario, pero dificulta depurar. El frontend sabe que la cuenta existe, mientras el equipo de producto no sabe si el email fue encolado, reintentado o descartado.
Para un flujo de onboarding, separo cinco estados sencillos:
-
created: la cuenta fue creada. -
queued: el evento de email entró en la cola. -
sent: el proveedor aceptó el mensaje. -
delivered: recibimos confirmación de entrega cuando está disponible. -
failed: terminó el número de reintentos o hubo un error permanente.
Esto evita que un solo booleano como welcome_email_sent esconda demasiadas situaciones. Tambien hace más fácil explicar el estado a soporte: no es lo mismo “todavía en cola” que “el proveedor rechazó el dominio”.
Qué debe contener el contrato de eventos
Cada evento necesita un identificador estable y algunos datos mínimos. Por ejemplo:
{
"event_id": "evt_01J9W2Q4K8",
"type": "onboarding.email.requested",
"account_id": "acc_2048",
"template": "welcome-v2",
"attempt": 1,
"created_at": "2026-10-18T20:00:00Z"
}
El event_id sirve para aplicar idempotencia. Si un worker recibe el mismo mensaje dos veces, no deberia crear dos envíos. template ayuda a comparar versiones y attempt permite distinguir un reintento de un evento nuevo.
También guardo una razón de fallo legible, pero sin incluir el contenido completo del email ni datos privados. En desarrollo, esa pequeña decisión ahorra bastante tiempo: el equipo puede revisar la secuencia sin copiar direcciones reales a una hoja o a un chat.
Si tu producto necesita más detalle, puedes añadir correlation_id, provider_message_id y environment. No añadiría campos solo porque “quizas algún día” sirvan. Un contrato pequeño se mantiene mejor y se aprende más rapido.
Implementación sencilla en el backend
La tabla de eventos puede vivir junto a las demás tablas del SaaS. Un índice único sobre event_id y una transición controlada de estados suele ser suficiente para comenzar. La idea importante es que la aplicación no marque un email como enviado antes de que el worker lo entregue al proveedor.
Un pseudocódigo de consumo puede verse así:
event = inbox.next()
if events.exists(event.event_id):
acknowledge(event)
else:
events.insert(event, status="queued")
result = mailer.send(event)
events.update(event.event_id, result.status, result.message_id)
acknowledge(event)
En producción, el detalle depende de la cola y del proveedor. Para un proyecto pequeño, lo esencial es registrar la respuesta y no esconder los fallos detrás de un reintento infinito. Una cola que insiste para siempre puede hacer que un problema de configuración parezca tráfico normal.
Para medir productividad del equipo, observo tres tiempos: desde created hasta queued, desde queued hasta sent y desde sent hasta delivered. No uso estos números como una promesa de entrega; los uso para encontrar donde se atasca el flujo.
Cómo probar el flujo completo
Primero pruebo el contrato sin depender de un proveedor externo. El test publica un evento, ejecuta el worker y verifica que el estado final, el número de intentos y el identificador de correlación sean correctos.
Después hago una prueba de integración con una bandeja aislada. Una dirección temporal puede servir para inspeccionar el asunto, el enlace de activación y la versión de la plantilla sin tocar la bandeja personal de un desarrollador. En búsquedas de este tema aparecen variantes como tempmail.so, tempmail so y hasta el typo tepm mail com; son términos útiles para entender la intención, pero no deberían convertirse en nombres de variables o estados del sistema.
Para el flujo visual, conviene comprobar que la interfaz explica cada espera: “procesando”, “enviado” y “no pudimos entregar”. Puedes probar emails de referidos sin llenar bandejas reales y revisar también cómo diseñar estados accesibles para un inbox de prueba.
Un checklist útil para cada ejecución es:
- ¿El evento aparece una sola vez?
- ¿El worker puede reintentarlo sin duplicar el email?
- ¿El enlace apunta al entorno correcto?
- ¿La plantilla corresponde a la versión esperada?
- ¿El fallo explica la siguiente acción?
- ¿La prueba usa una bandeja separada de las cuentas reales?
Al principio, algunas pruebas pueden parecer repetitivas. Aun así, suelen detectar regresiones antes de que soporte reciba el primer mensaje. Es mejor invertir cinco minutos en una evidencia clara que media hora intentando adivinar qué pasó.
Errores comunes
El primer error es probar solo la función que llama al proveedor. Eso deja fuera la cola, los reintentos y el estado que ve el usuario. El segundo es usar una dirección personal durante el desarrollo. Contamina resultados, puede exponer datos y hace que el equipo confunda una prueba manual con una señal del producto.
El tercero es medir solo “enviado”. Para un SaaS, la experiencia depende de todo el recorrido. Si no hay confirmación de entrega, registra esa limitación y presenta un estado honesto; no inventes certeza.
Recapitulación
Un contrato de eventos convierte un email de onboarding en algo observable y comprobable. Empieza con estados pequeños, un identificador idempotente y una razón de fallo clara. Luego prueba el backend, el worker, la bandeja aislada y la experiencia visual por separado.
La meta no es tener más logs. Es que una persona nueva del equipo pueda responder, rapido y sin adivinar, qué ocurrió con cada email. Cuando esa respuesta está disponible, mejorar el onboarding deja de ser una discusión de intuiciones y se convierte en trabajo de producto medible.
Preguntas rápidas
¿Necesito una cola para un SaaS pequeño? No siempre. Pero conviene diseñar el contrato de eventos aunque el primer worker sea simple; así la migración será menos dolorosa cuando aumente el tráfico.
¿Debo probar con direcciones reales? Para pruebas automáticas, no. Usa datos y bandejas aisladas. Reserva las cuentas reales para una verificación manual controlada y documentada.
Top comments (0)