<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hannah</title>
    <description>The latest articles on DEV Community by Hannah (@hannahdev56).</description>
    <link>https://dev.to/hannahdev56</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4013601%2F60834200-9095-4445-9182-c686625242ee.png</url>
      <title>DEV Community: Hannah</title>
      <link>https://dev.to/hannahdev56</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hannahdev56"/>
    <language>en</language>
    <item>
      <title>SaaS: un contrato de estados para el onboarding</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 20:22:54 +0000</pubDate>
      <link>https://dev.to/hannahdev56/saas-un-contrato-de-estados-para-el-onboarding-4p07</link>
      <guid>https://dev.to/hannahdev56/saas-un-contrato-de-estados-para-el-onboarding-4p07</guid>
      <description>&lt;p&gt;Cuando un SaaS tiene pocos usuarios, el onboarding parece una secuencia simple: crear cuenta, enviar un correo y activar el perfil. Pero en cuanto aparecen reintentos, invitaciones, planes distintos y pruebas automáticas, la pregunta cambia: ¿en qué estado exacto quedó cada persona?&lt;/p&gt;

&lt;p&gt;En mis experimentos de producto, me ha ayudado tratar el onboarding como un contrato de estados. No es una gran máquina distribuida. Es una lista pequeña y explicable que conecta la interfaz, el backend y las pruebas de email. Con ese modelo, un equipo puede entender qué paso ocurrio, repetir una prueba y medir la activación sin adivinar.&lt;/p&gt;

&lt;h2&gt;
  
  
  El onboarding también necesita un contrato
&lt;/h2&gt;

&lt;p&gt;Un contrato define qué significa cada estado y qué transiciones están permitidas. Por ejemplo, &lt;code&gt;created&lt;/code&gt; no debería significar lo mismo que &lt;code&gt;email_sent&lt;/code&gt;, y &lt;code&gt;email_verified&lt;/code&gt; no debería llegar antes de que exista un token válido.&lt;/p&gt;

&lt;p&gt;Un flujo inicial puede ser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created -&amp;gt; email_pending -&amp;gt; email_sent -&amp;gt; verified -&amp;gt; activated
                         \-&amp;gt; email_failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La flecha no es solo documentación. Es una regla para el código. Si el frontend muestra “cuenta activa” mientras la base de datos todavía dice &lt;code&gt;email_pending&lt;/code&gt;, el usuario recibe una señal confusa y soporte pierde tiempo buscando la causa.&lt;/p&gt;

&lt;p&gt;También conviene guardar &lt;code&gt;state_changed_at&lt;/code&gt;, &lt;code&gt;attempt_count&lt;/code&gt; y &lt;code&gt;last_reason&lt;/code&gt;. Así el equipo backend puede ver si el retraso viene del proveedor, de un worker detenido o de una acción repetida en la interfaz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué estados conviene modelar
&lt;/h2&gt;

&lt;p&gt;No hace falta crear un estado para cada detalle. Empieza con los momentos que cambian una decisión de producto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;created&lt;/code&gt;: la cuenta existe, pero el flujo no ha solicitado verificación.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;email_pending&lt;/code&gt;: hay un trabajo pendiente de enviar.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;email_sent&lt;/code&gt;: el proveedor aceptó el mensaje.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;verified&lt;/code&gt;: la persona confirmó el token.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;activated&lt;/code&gt;: completó el primer paso útil del producto.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expired&lt;/code&gt;: el token o la invitación ya no son válidos.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;failed&lt;/code&gt;: el sistema agotó una política de reintentos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La separación entre &lt;code&gt;verified&lt;/code&gt; y &lt;code&gt;activated&lt;/code&gt; es especialmente útil. Una persona puede confirmar su correo y nunca crear su primer proyecto. Si agrupamos todo como “registrado”, perdemos la señal que sirve para mejorar el onboarding. En mi caso, esa distinción hizo mas fácil conversar con producto: el problema no era el correo, sino el siguiente paso.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una identidad de prueba por ejecución
&lt;/h2&gt;

&lt;p&gt;Las pruebas automáticas suelen fallar porque varias ejecuciones comparten el mismo buzón o la misma dirección. Una ejecución recibe el correo de otra, consume un token antes de tiempo y deja un error que aparece solo a veces.&lt;/p&gt;

&lt;p&gt;La solución práctica es dar a cada ejecución una identidad propia. El registro de prueba puede guardar algo como esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup-2026-10-09-042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"test_email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup-042@example.test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"verified"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-09T21:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La identidad no tiene que ser permanente. Una herramienta de correo de prueba como tempmailso puede servir para validar el recorrido, siempre que el equipo conozca el límite de retención y no mezcle datos reales con fixtures. Incluso un valor de búsqueda mal escrito, como temp mailid, debe quedar asociado a una ejecución y no a una cuenta compartida.&lt;/p&gt;

&lt;p&gt;Cuando cada prueba tiene su propio &lt;code&gt;run_id&lt;/code&gt;, investigar un fallo es más directo. Se puede preguntar qué mensaje llegó, qué transición esperaba la prueba y cuánto tiempo pasó entre ambos eventos. Ese contexto vale más que un simple “assert failed”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotencia y limpieza
&lt;/h2&gt;

&lt;p&gt;El endpoint de “reenviar correo” debe ser seguro si el usuario pulsa dos veces. Una clave como &lt;code&gt;signup:{user_id}:{flow_id}&lt;/code&gt; permite reconocer el mismo trabajo. El worker también debería comprobar el estado actual antes de enviar: si ya está en &lt;code&gt;email_sent&lt;/code&gt;, no debe crear otro mensaje por accidente.&lt;/p&gt;

&lt;p&gt;Para las pruebas, la clave puede incluir el &lt;code&gt;run_id&lt;/code&gt;. Así una nueva ejecución no hereda el estado de la anterior. Si la prueba se repite con &lt;code&gt;signup-043&lt;/code&gt;, su buzón y sus eventos son nuevos.&lt;/p&gt;

&lt;p&gt;La limpieza es la otra mitad del contrato. Define cuándo se eliminan los mensajes, tokens y registros de prueba. Una retención corta reduce ruido, pero borrar enseguida puede hacer imposible depurar un fallo. Un equilibrio sencillo es conservar el resumen de la ejecución y eliminar el contenido del mensaje cuando ya no sea necesario.&lt;/p&gt;

&lt;p&gt;En una base pequeña, incluso un job diario puede marcar como expirados los registros viejos. Lo importante es que no dependa de una persona acordándose manualmente. Cuando los estados queda sin dueño, los datos de prueba terminan pareciendo usuarios reales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes
&lt;/h2&gt;

&lt;p&gt;El primer error es modelar solo el estado final. “Activo” no explica si la persona verificó el correo, aceptó una invitación o fue activada por un administrador.&lt;/p&gt;

&lt;p&gt;El segundo es guardar el estado únicamente en el frontend. El navegador puede cerrarse, repetir una solicitud o mostrar información antigua. La fuente de verdad debe vivir en el backend, y la interfaz debe renderizar una versión clara de ella.&lt;/p&gt;

&lt;p&gt;El tercero es usar direcciones fijas para todos los tests. Un dato de prueba llamado fake e mail com puede parecer inofensivo, pero si se reutiliza entre suites genera falsos positivos y hace difícil saber que pasó.&lt;/p&gt;

&lt;p&gt;Por último, no midas solo el porcentaje de emails enviados. Añade tiempo hasta verificación, tasa de expiración, reintentos, activación posterior y ejecuciones contaminadas por datos anteriores. Esas señales ayudan a priorizar mejoras reales. También evita que el equipo persiga problemas de correo cuando el bloqueo está en otra pantalla.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist práctico
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Cada estado tiene una definición breve y una transición válida.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;verified&lt;/code&gt; y &lt;code&gt;activated&lt;/code&gt; representan momentos diferentes.&lt;/li&gt;
&lt;li&gt;[ ] Cada email de prueba tiene un &lt;code&gt;run_id&lt;/code&gt; propio.&lt;/li&gt;
&lt;li&gt;[ ] El reenvío usa una clave idempotente.&lt;/li&gt;
&lt;li&gt;[ ] Los errores guardan una razón segura y un contador de intentos.&lt;/li&gt;
&lt;li&gt;[ ] Existe una política de expiración y limpieza.&lt;/li&gt;
&lt;li&gt;[ ] Las métricas separan entrega, verificación y activación.&lt;/li&gt;
&lt;li&gt;[ ] La interfaz comunica el estado actual sin prometer mas de lo que el backend sabe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si el flujo usa React, también merece la pena &lt;a href="https://dev.to/silviutech/react-reenvios-de-verificacion-sin-ansiedad-4km0"&gt;diseñar reenvíos de verificación sin ansiedad&lt;/a&gt;: el estado del email debe ser visible, pero no interrumpir el trabajo de la persona. Y para analizar el resultado, puedes &lt;a href="https://dev.to/hannahdev56/saas-metricas-de-activacion-por-email-112j"&gt;medir activación por email con más contexto&lt;/a&gt;, separando la confirmación del primer momento de valor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recapitulación
&lt;/h2&gt;

&lt;p&gt;Un contrato de estados convierte el onboarding en algo que se puede explicar, probar y mejorar. Empieza con pocos estados, guarda el motivo de cada transición y da a cada ejecución de prueba una identidad aislada.&lt;/p&gt;

&lt;p&gt;No necesitas una arquitectura enorme. Necesitas que el backend sepa qué paso ocurrió, que el frontend lo comunique con honestidad y que la limpieza tenga un responsable automático. Con esas bases, un SaaS puede crecer sin convertir cada “no recibí el email” en una investigación desde cero.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>SaaS: contrato de entrega para emails</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Wed, 07 Oct 2026 14:23:54 +0000</pubDate>
      <link>https://dev.to/hannahdev56/saas-contrato-de-entrega-para-emails-4fl4</link>
      <guid>https://dev.to/hannahdev56/saas-contrato-de-entrega-para-emails-4fl4</guid>
      <description>&lt;p&gt;En un SaaS, un email de activación no es solo un mensaje que sale de un servidor. Es una parte del producto. Si llega tarde, se duplica o nadie puede explicar que paso, la persona usuaria puede abandonar el onboarding aunque el registro haya funcionado.&lt;/p&gt;

&lt;p&gt;Un contrato de entrega ayuda a quitar esa incertidumbre. No promete que todos los mensajes llegarán siempre, porque eso depende de proveedores y destinatarios. Promete algo mas útil para el equipo: cada intento tendrá un estado, un contexto y una siguiente acción clara.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué debe prometer el contrato de entrega
&lt;/h2&gt;

&lt;p&gt;Antes de escribir workers o añadir otra cola, define qué significa “entregado” en tu producto. Yo separaría estas etapas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;requested&lt;/code&gt;: el producto pidió el email.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;queued&lt;/code&gt;: el backend guardó el trabajo.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sending&lt;/code&gt;: un worker lo tomó.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;accepted&lt;/code&gt;: el proveedor aceptó la petición.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;observed&lt;/code&gt;: una prueba o evento confirmó el resultado esperado.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;accepted&lt;/code&gt; no es lo mismo que “la persona lo leyó”. Esta diferencia parece pequeña, pero evita que soporte prometa demasiado. También permite investigar si el problema está en la aplicación, en el proveedor o en el buzón de destino.&lt;/p&gt;

&lt;p&gt;Un registro mínimo puede verse así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;delivery_id     identificador único
user_id         usuario del SaaS
kind            welcome, verify o reset
template        versión de la plantilla
status          estado actual
attempts        número de intentos
last_error      error breve y seguro
created_at      momento de creación
updated_at      última transición
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La plantilla debe tener una versión. Si el usuario recibe un texto antiguo, podremos saber qué contenido generó el worker sin buscar en cada despliegue. Este detalle vuelve el debugging mucho menos dificil.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseña el flujo paso a paso
&lt;/h2&gt;

&lt;p&gt;El flujo puede empezar en la transacción que crea la cuenta, pero no debería enviar el email dentro de la misma petición HTTP. Guarda un evento o trabajo y responde al frontend. Después, un worker procesa la cola.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signup -&amp;gt; delivery.requested -&amp;gt; delivery.queued
                              -&amp;gt; worker toma el trabajo
                              -&amp;gt; accepted o failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para evitar duplicados, usa una clave de idempotencia basada en el caso de uso. Por ejemplo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;welcome:user_482:template_v3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Antes de crear otra tarea, busca una entrega activa con esa clave. El worker también debe comprobar el estado justo antes de llamar al proveedor. Si un proceso ya la marcó como &lt;code&gt;accepted&lt;/code&gt;, no conviene reintentar de nuevo solo porque un timeout escondió la respuesta.&lt;/p&gt;

&lt;p&gt;Los reintentos tienen que distinguir errores temporales de errores permanentes. Un timeout de red puede volver a intentarse con una espera creciente. Una dirección rechazada necesita una ruta de corrección, no cinco llamadas iguales. Anota la razón, el número de intento y el próximo momento de ejecución.&lt;/p&gt;

&lt;p&gt;Para diseñar los estados y sus salidas, resulta útil pensar en &lt;a href="https://dev.to/silviutech/llms-contratos-de-salida-para-cron-fiables-4iec"&gt;contratos de salida para automatizaciones&lt;/a&gt;. La idea es parecida: cada paso debe producir una señal que el siguiente componente pueda interpretar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prueba el onboarding con un buzón aislado
&lt;/h2&gt;

&lt;p&gt;Las pruebas end-to-end suelen fallar cuando varias ejecuciones comparten la misma bandeja. Un test puede leer el mensaje de otro branch y parecer correcto. La solución práctica es crear un buzón o dirección de prueba por ejecución y guardar su identidad junto al &lt;code&gt;delivery_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;En algunos equipos se usa un &lt;strong&gt;correo temporal gratuito&lt;/strong&gt; o un &lt;strong&gt;correo desechable gratuito&lt;/strong&gt; para validar el flujo en staging. Eso puede servir, pero el test debe tratar esa dirección como un fixture aislado: no la mezcles con métricas de producción, no guardes secretos en el código y define cuánto tiempo se conserva.&lt;/p&gt;

&lt;p&gt;También conviene guardar un pequeño recibo de la prueba:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ci-1842"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"delivery_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"del_8f21"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Confirma tu cuenta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"observed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attempts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si alguien escribe &lt;strong&gt;tepm mail com&lt;/strong&gt; en una nota de QA, esa cadena no debe romper el parser ni entrar como una dirección real. Los fixtures deben validar formato y separar las anotaciones humanas de los datos que usa el worker.&lt;/p&gt;

&lt;p&gt;Para flujos con varios pasos, también vale la pena &lt;a href="https://dev.to/silviutech/versiona-acciones-de-correo-en-agentes-llm-63d"&gt;versionar acciones de correo&lt;/a&gt;. Versionar no significa hacer una plataforma enorme; significa poder reproducir que configuración produjo cada resultado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mide evidencia, no solo envíos
&lt;/h2&gt;

&lt;p&gt;Una métrica de “emails enviados” es insuficiente. Añadiría estas señales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tiempo desde &lt;code&gt;requested&lt;/code&gt; hasta &lt;code&gt;accepted&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;porcentaje de entregas que necesitaron reintento;&lt;/li&gt;
&lt;li&gt;duplicados evitados por la clave de idempotencia;&lt;/li&gt;
&lt;li&gt;entregas sin confirmación después del límite;&lt;/li&gt;
&lt;li&gt;fallos agrupados por tipo, no por contenido privado.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La privacidad importa también en los logs. Guarda identificadores y dominios cuando sea necesario, pero evita tokens, enlaces completos de recuperación y cuerpos enteros. Un panel con demasiados datos personales es dificil de compartir durante un incidente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes
&lt;/h2&gt;

&lt;p&gt;El primer error es llamar al proveedor antes de persistir el trabajo. Si el proceso cae después del envío, no hay forma confiable de saber qué ocurrió. El segundo es usar un solo estado &lt;code&gt;failed&lt;/code&gt; para todo; así se pierde la diferencia entre un rechazo permanente y una caída temporal.&lt;/p&gt;

&lt;p&gt;Otro error frecuente es borrar la evidencia apenas el test termina. Conserva un resumen corto durante el tiempo que tu equipo necesite para depurar, y elimina lo que ya no aporta valor. Finalmente, no conviertas cada reintento en una notificación para la persona usuaria: los estados internos pueden ser detallados mientras la interfaz mantiene un mensaje simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist de implementación
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Cada entrega tiene un identificador y una clave de idempotencia.&lt;/li&gt;
&lt;li&gt;[ ] Los estados distinguen petición, cola, proveedor y observación.&lt;/li&gt;
&lt;li&gt;[ ] La plantilla se guarda con una versión.&lt;/li&gt;
&lt;li&gt;[ ] Los reintentos tienen límite y clasificación de errores.&lt;/li&gt;
&lt;li&gt;[ ] Cada prueba usa un fixture de inbox aislado.&lt;/li&gt;
&lt;li&gt;[ ] Los logs no contienen tokens ni cuerpos completos.&lt;/li&gt;
&lt;li&gt;[ ] El frontend explica qué puede hacer la persona si el email no llega.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Preguntas rápidas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Necesito una cola especializada desde el inicio?
&lt;/h3&gt;

&lt;p&gt;No. Una tabla en PostgreSQL y un worker sencillo pueden ser suficientes para un SaaS pequeño. Empieza con estados claros y cambia de herramienta cuando una necesidad observada lo justifique.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿&lt;code&gt;accepted&lt;/code&gt; significa que el email llegó?
&lt;/h3&gt;

&lt;p&gt;No necesariamente. Significa que el proveedor aceptó la solicitud. La entrega final depende de más sistemas, por eso conviene separar aceptación, observación y lectura.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué hago si el test encuentra un mensaje viejo?
&lt;/h3&gt;

&lt;p&gt;Relaciona cada ejecución con un buzón, asunto o identificador único. Busca por el &lt;code&gt;delivery_id&lt;/code&gt; o por un encabezado de prueba, no solamente por el asunto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recapitulación
&lt;/h2&gt;

&lt;p&gt;Un contrato de entrega convierte el email de onboarding en una pieza observable del producto. Con estados pequeños, idempotencia, fixtures aislados y métricas útiles, el equipo puede saber qué pasó y qué hacer después. Es una mejora de backend, pero también de productividad: menos adivinanzas, menos reintentos ciegos y conversaciones de soporte mas claras.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>SaaS: cómo probar onboarding sin emails falsos</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Tue, 06 Oct 2026 11:23:51 +0000</pubDate>
      <link>https://dev.to/hannahdev56/saas-como-probar-onboarding-sin-emails-falsos-23gf</link>
      <guid>https://dev.to/hannahdev56/saas-como-probar-onboarding-sin-emails-falsos-23gf</guid>
      <description>&lt;p&gt;Cuando una persona se registra en un SaaS, el correo de bienvenida parece un detalle pequeño. En realidad, suele ser el puente entre “creé una cuenta” y “entendí el valor del producto”. Si esa prueba depende de una bandeja compartida o de un email fijo, el resultado se vuelve dificil de interpretar: el test puede pasar aunque haya recibido un mensaje viejo.&lt;/p&gt;

&lt;p&gt;La solución no es añadir más asserts sin orden. Es darle a cada ejecución un buzón propio, una expectativa clara y un recibo que explique qué ocurrió.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: onboarding verde, producto roto
&lt;/h2&gt;

&lt;p&gt;Un flujo de onboarding normalmente tiene varios pasos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Crear una cuenta con un correo nuevo.&lt;/li&gt;
&lt;li&gt;Esperar el mensaje de verificación.&lt;/li&gt;
&lt;li&gt;Seguir el enlace.&lt;/li&gt;
&lt;li&gt;Completar el primer paso útil del producto.&lt;/li&gt;
&lt;li&gt;Comprobar que la cuenta aparece activa.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cuando varias pruebas usan el mismo buzón, aparecen falsos positivos y falsos negativos. Un mensaje anterior puede contener un enlace todavía válido. Otra ejecución puede leer el correo equivocado. Y si el equipo mira la analítica, los usuarios de prueba se mezclan con señales de marketing o con clientes reales.&lt;/p&gt;

&lt;p&gt;El resultado más peligroso es un onboarding “verde” que no representa la experiencia que tendrá una persona nueva.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué debe representar un buzón de prueba
&lt;/h2&gt;

&lt;p&gt;Un buzón de prueba no necesita ser una copia perfecta de Gmail. Necesita representar tres cosas de forma consistente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identidad:&lt;/strong&gt; la dirección debe estar asociada a una sola ejecución.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor:&lt;/strong&gt; la prueba debe buscar mensajes posteriores a su inicio, no cualquier mensaje disponible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retención:&lt;/strong&gt; al terminar, los datos deben expirar o quedar marcados para limpieza.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para una prueba local, un correo temporal puede ser suficiente. En CI, conviene además guardar el identificador de la ejecución y el asunto esperado. Un buzón aislado por ejecución hace que el diagnóstico sea más rapido, porque la pregunta deja de ser “¿qué mensaje recibió esta cuenta?” y pasa a ser “¿qué ocurrió en esta ejecución?”.&lt;/p&gt;

&lt;p&gt;La cadena &lt;code&gt;tepm mail com&lt;/code&gt; puede aparecer en búsquedas internas o en notas de soporte por un error de escritura; no conviene usarla como criterio de validación ni como dominio real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato sencillo por ejecución
&lt;/h2&gt;

&lt;p&gt;Antes de escribir el test, define un contrato pequeño:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run_id       = ci-1842
email        = ci-1842@dominio-de-pruebas.example
subject      = Confirma tu cuenta
max_wait     = 60 segundos
message_from = no-reply@tu-saas.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La prueba crea la dirección, registra &lt;code&gt;run_id&lt;/code&gt; y espera sólo mensajes que cumplan el contrato. No basta con comprobar que existe un email: también hay que confirmar que el enlace apunta al entorno correcto y que la cuenta cambia al estado esperado.&lt;/p&gt;

&lt;p&gt;Un recibo mínimo puede verse así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ci-1842"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ci-1842@dominio-de-pruebas.example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"msg-9081"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"passed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"activation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"passed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cleanup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El campo &lt;code&gt;cleanup&lt;/code&gt; es importante. Si los datos se conserva sin límite, una suite pequeña termina creando ruido y costes que nadie mira.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ejemplo de implementación
&lt;/h2&gt;

&lt;p&gt;En un backend sencillo, separaría el flujo en tres funciones: &lt;code&gt;createFixture&lt;/code&gt;, &lt;code&gt;waitForWelcome&lt;/code&gt; y &lt;code&gt;assertActivation&lt;/code&gt;. La primera crea la identidad; la segunda usa un cursor de inicio y un límite de tiempo; la tercera valida el efecto en la aplicación.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createFixture&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitForWelcome&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;openVerificationLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;assertActivation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markForCleanup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En caso de fallo, conserva el recibo y una versión redactada del asunto. No guardes el enlace completo de verificación si contiene un token válido. Para alertas operativas, ayuda pensar en el mismo patrón que al &lt;a href="https://dev.to/alexcarteruk/kubernetes-alertas-de-rollback-con-contexto-real-2em2"&gt;diseñar alertas de rollback con contexto&lt;/a&gt;: el dato debe indicar qué falló, dónde y con qué contexto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes que salen caros
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Usar una cuenta fija.&lt;/strong&gt; Es cómodo al principio, pero crea carreras entre tests y hace dificil reproducir un fallo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Esperar sólo por el asunto.&lt;/strong&gt; Dos mensajes pueden compartir asunto. Añade remitente, fecha de creación y un identificador de ejecución.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verificar la entrega, pero no la activación.&lt;/strong&gt; El mensaje puede llegar y aun así tener un enlace roto, una URL de staging o un estado de usuario que no cambia.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Borrar demasiado pronto.&lt;/strong&gt; Si limpias antes de crear el recibo, el diagnóstico queda incompleto. Marca para limpiar y aplica la retención después de conservar los metadatos necesarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mezclar pruebas con campañas.&lt;/strong&gt; Un correo de test nunca deberia entrar en la analítica normal de marketing. Usa un atributo de entorno y exclúyelo de los informes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo conectar la prueba con el producto
&lt;/h2&gt;

&lt;p&gt;La prueba técnica gana valor cuando responde una pregunta de producto: ¿una persona nueva puede llegar al primer momento útil sin fricción? Para medirlo, registra tiempos separados para entrega, clic y activación. No hace falta construir un sistema enorme; una tabla de eventos por &lt;code&gt;run_id&lt;/code&gt; ya ayuda a ver dónde se pierde el flujo.&lt;/p&gt;

&lt;p&gt;También puedes crear un pequeño &lt;a href="https://dev.to/alexcarteruk/kubernetes-correos-utiles-para-alertas-nocturnas-pan"&gt;catálogo de correos útiles para alertas operativas&lt;/a&gt; y reutilizar la misma disciplina de contexto en los fallos de onboarding.&lt;/p&gt;

&lt;p&gt;Si necesitas una herramienta externa para preparar identidades de prueba, un &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;generador de correo desechable&lt;/a&gt; puede servir para exploraciones manuales. Para CI, confirma antes sus límites de retención, privacidad y automatización; no pongas datos de clientes ni tokens reales en una bandeja compartida.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recapitulación
&lt;/h2&gt;

&lt;p&gt;Un buen test de onboarding SaaS no comprueba solamente que “llegó un correo”. Crea una identidad aislada, usa un cursor temporal, valida la activación y deja un recibo útil para depurar.&lt;/p&gt;

&lt;p&gt;Empieza con una sola ejecución por buzón, un asunto y remitente esperados, un límite de espera y una limpieza explícita. Después conecta esos resultados con la analítica de activación. Es un cambio pequeño, pero vuelve las pruebas más confiables y el producto más fácil de mejorar en el día a día.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>testing</category>
      <category>backend</category>
      <category>productivity</category>
    </item>
    <item>
      <title>SaaS: prueba emails con un contrato de ejecución</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Tue, 06 Oct 2026 08:23:17 +0000</pubDate>
      <link>https://dev.to/hannahdev56/saas-prueba-emails-con-un-contrato-de-ejecucion-18cm</link>
      <guid>https://dev.to/hannahdev56/saas-prueba-emails-con-un-contrato-de-ejecucion-18cm</guid>
      <description>&lt;p&gt;En un SaaS, los emails parecen una pieza pequeña hasta que el primer test falla por leer el mensaje equivocado. Una ejecución crea una cuenta, otra confirma el mismo buzón temporal y una tercera recibe el correo de la primera. El resultado es un test intermitente que cuesta mas tiempo investigar que arreglar.&lt;/p&gt;

&lt;p&gt;En mis experimentos de producto he encontrado una regla simple: cada ejecución de prueba debe tener dueño, identidad y una forma clara de terminar. No hace falta crear una plataforma enorme. Hace falta acordar un contrato entre el test, el Backend y el proveedor de correo.&lt;/p&gt;

&lt;p&gt;Este enfoque complementa ideas para &lt;a href="https://dev.to/hannahdev56/backend-de-saas-emails-de-prueba-sin-caos-kkn"&gt;aislar el correo de cada ejecución&lt;/a&gt; y también ayuda cuando un flujo incluye un &lt;a href="https://dev.to/pak3ng/react-signup-accesible-para-pruebas-reales-4ijd"&gt;signup accesible para pruebas reales&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema de compartir una bandeja
&lt;/h2&gt;

&lt;p&gt;Una bandeja compartida rompe la relación entre causa y evidencia. Si el test solo pregunta “¿hay un email nuevo?”, no sabe si ese email pertenece a su propia ejecución. Un reintento puede consumir el mensaje correcto antes que el test original, o una ejecución lenta puede encontrar datos que dejó otra.&lt;/p&gt;

&lt;p&gt;Esto ocurre mucho en CI porque varios jobs corren al mismo tiempo. También ocurre en local: alguien repite el test y el correo anterior sigue ahí. Las cadenas de prueba suelen tener nombres raros, como &lt;code&gt;fake e mail com&lt;/code&gt;, y luego nadie recuerda cual job las creó.&lt;/p&gt;

&lt;p&gt;La solución no es esperar más. Añadir &lt;code&gt;sleep(10)&lt;/code&gt; solo hace que el problema sea mas caro. La solución es dar contexto al email y validar ese contexto antes de aceptarlo.&lt;/p&gt;

&lt;h2&gt;
  
  
  El contrato de ejecución
&lt;/h2&gt;

&lt;p&gt;Un contrato pequeño puede tener cuatro campos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;run_id&lt;/code&gt;: identificador único del job o de la ejecución.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mailbox_id&lt;/code&gt;: buzón temporal reservado para ese &lt;code&gt;run_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;created_at&lt;/code&gt;: momento en que el test comenzó a esperar mensajes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expires_at&lt;/code&gt;: límite después del cual los datos ya no sirven.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Por ejemplo, el backend puede crear este registro antes de abrir el formulario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ci-1842-signup-07"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mailbox_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qa-ci-1842-07"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup-verification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-06T09:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El nombre exacto puede cambiar. Lo importante es que el test conserve el &lt;code&gt;run_id&lt;/code&gt; y lo use en cada consulta. Un correo temporal como &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;tempmailso&lt;/a&gt; puede servir para pruebas controladas, siempre que el equipo trate el contenido como dato de test y no como una cuenta real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un diseño pequeño para el backend
&lt;/h2&gt;

&lt;p&gt;El endpoint de creación debería devolver una identidad de ejecución, no solo una dirección de correo. Así el cliente de pruebas no necesita adivinar qué mensaje pertenece a qué job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /test-fixtures/mailboxes
Idempotency-Key: ci-1842-signup-07

{
  "purpose": "signup-verification",
  "ttl_seconds": 900
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La respuesta puede incluir &lt;code&gt;run_id&lt;/code&gt;, &lt;code&gt;mailbox_id&lt;/code&gt; y una fecha de expiración. En la consulta, el backend debe filtrar por el buzón y por un cursor o timestamp. De esta forma un mensaje viejo no pasa solo porque contiene la palabra “verify”.&lt;/p&gt;

&lt;p&gt;También conviene que la limpieza tenga un responsable. Un worker puede borrar fixtures expirados, pero el job debe intentar liberar su buzón en un bloque &lt;code&gt;finally&lt;/code&gt;. Si el job muere antes, el TTL sigue siendo la red de seguridad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué debe comprobar el test
&lt;/h2&gt;

&lt;p&gt;Un test de verificación de email debería comprobar, en este orden:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Que el &lt;code&gt;mailbox_id&lt;/code&gt; corresponde a su propio &lt;code&gt;run_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Que el mensaje llegó después de iniciar la ejecución.&lt;/li&gt;
&lt;li&gt;Que el destinatario esperado coincide, sin comparar solo el asunto.&lt;/li&gt;
&lt;li&gt;Que el token no está expirado y puede usarse una sola vez.&lt;/li&gt;
&lt;li&gt;Que el mensaje queda marcado como consumido.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El cuarto punto suele olvidarse. Un correo correcto pero antiguo puede producir un falso positivo. La prueba pasa, aunque el flujo actual nunca haya enviado nada.&lt;/p&gt;

&lt;p&gt;Para depurar, guarda un recibo pequeño: &lt;code&gt;run_id&lt;/code&gt;, estado, timestamps, proveedor y razón del fallo. No guardes el cuerpo completo si no hace falta. El recibo permite saber si el problema fue creación, entrega, lectura o limpieza, y hace el resultado bastante mas útil para quien mantiene el SaaS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes
&lt;/h2&gt;

&lt;p&gt;El primer error es usar una dirección fija para todos los tests. Es cómodo al principio, pero se vuelve una cola compartida. El segundo es usar el asunto como identificador único; dos mensajes pueden tener el mismo asunto.&lt;/p&gt;

&lt;p&gt;El tercero es mezclar formato y entrega. Validar que la dirección tiene forma correcta no demuestra que el email llegó. Son comprobaciones diferentes y deben tener tiempos y mensajes distintos.&lt;/p&gt;

&lt;p&gt;El cuarto es borrar evidencia demasiado pronto. Durante desarrollo, conserva el recibo hasta que el job termine y registra solo los datos necesarios. En producción, aplica una retención corta y documentada.&lt;/p&gt;

&lt;p&gt;Por último, no escondas toda la lógica detrás de reintentos. Un reintento con el mismo contrato puede ser seguro; un reintento que crea otra identidad sin marcar la anterior deja basura y vuelve confuso el diagnóstico.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recapitulación
&lt;/h2&gt;

&lt;p&gt;Un contrato de ejecución convierte las pruebas de email de una búsqueda ambigua en una conversación clara entre el test y el Backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cada job tiene un &lt;code&gt;run_id&lt;/code&gt; único;&lt;/li&gt;
&lt;li&gt;cada buzón temporal tiene una fecha de caducidad;&lt;/li&gt;
&lt;li&gt;cada lectura valida identidad, tiempo y destinatario;&lt;/li&gt;
&lt;li&gt;cada fixture tiene limpieza y un recibo mínimo;&lt;/li&gt;
&lt;li&gt;los reintentos conservan el contexto original.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Empieza con un solo flujo de signup. Añade el registro, filtra los mensajes por ejecución y mide los fallos durante una semana. Cuando el patrón sea estable, puedes llevarlo a otros emails del SaaS sin cambiar toda la arquitectura. Es un cambio pequeño, pero hace que los tests sean mucho mas fáciles de confiar.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>SaaS: una bandeja por ejecución para probar emails</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Sun, 04 Oct 2026 20:23:49 +0000</pubDate>
      <link>https://dev.to/hannahdev56/saas-una-bandeja-por-ejecucion-para-probar-emails-2f00</link>
      <guid>https://dev.to/hannahdev56/saas-una-bandeja-por-ejecucion-para-probar-emails-2f00</guid>
      <description>&lt;p&gt;Cuando un SaaS empieza a crecer, los emails de prueba suelen quedarse en una bandeja compartida. Parece práctico: todos miran el mismo lugar y nadie tiene que aprender otra herramienta. Pero despues de unos cuantos sprints aparecen mensajes mezclados, enlaces vencidos y pruebas que pasan solo porque encontraron el código de otra ejecución.&lt;/p&gt;

&lt;p&gt;Una bandeja por ejecución cambia esa historia. Cada test recibe un identificador, una dirección temporal y una regla clara de limpieza. No es una arquitectura enorme; es un pequeño contrato de backend que hace que las pruebas sean más faciles de repetir y de explicar.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema de una bandeja compartida
&lt;/h2&gt;

&lt;p&gt;Imagina un flujo de registro que crea una cuenta, envía un código y confirma el email. Tres ramas de CI empiezan casi al mismo tiempo. Si usan el mismo buzón, cualquiera puede leer el mensaje equivocado. Un test puede pasar con un código creado por otra rama, mientras otro falla porque su mensaje ya fue consumido.&lt;/p&gt;

&lt;p&gt;La situación empeora con los reintentos. Una ejecución fallida deja tres mensajes con el mismo asunto y un desarrollador no sabe cual pertenece al primer intento. Buscar frases como &lt;strong&gt;dummy e mail&lt;/strong&gt; o &lt;strong&gt;tempail mail&lt;/strong&gt; durante una sesión de debugging tampoco ayuda: esas expresiones deben ser datos de prueba, nunca una regla escondida que el sistema use para decidir qué mensaje escoger.&lt;/p&gt;

&lt;p&gt;En productos SaaS, el coste no es solamente la inestabilidad. También se acumulan cuentas, invitaciones y tokens que nadie limpia. La bandeja acaba siendo una base de datos informal, sin dueño y sin fecha de caducidad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseñar una bandeja por ejecución
&lt;/h2&gt;

&lt;p&gt;El primer paso es crear un &lt;code&gt;run_id&lt;/code&gt; al comienzo de cada ejecución. Puede ser corto y legible, por ejemplo &lt;code&gt;ci-1842-7f3a&lt;/code&gt;. Después, deriva de ese valor un identificador de fixture y una dirección de prueba:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run_id:      ci-1842-7f3a
fixture_id:  signup-ci-1842-7f3a
email:       signup-ci-1842-7f3a@example.test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No hace falta que el usuario real tenga acceso a esa dirección. El adaptador de email de staging debe poder consultar mensajes por &lt;code&gt;fixture_id&lt;/code&gt;, asunto o destinatario, y devolver solo los mensajes asociados a la ejecución actual.&lt;/p&gt;

&lt;p&gt;El contrato minimo debería responder estas preguntas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Quién creó el fixture y en qué entorno?&lt;/li&gt;
&lt;li&gt;¿Qué ejecución es la propietaria?&lt;/li&gt;
&lt;li&gt;¿Cuánto tiempo puede vivir?&lt;/li&gt;
&lt;li&gt;¿Qué método lo elimina después del test?&lt;/li&gt;
&lt;li&gt;¿Qué ocurre si el test termina a mitad del registro?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si el proveedor de email no permite un buzón nuevo por test, se puede mantener una bandeja controlada y usar destinatarios únicos. La separación lógica importa más que el número de buzones físicos, aunque el filtro debe comprobar siempre el destinatario exacto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato sencillo para el backend
&lt;/h2&gt;

&lt;p&gt;Una API pequeña puede exponer tres operaciones: reservar, leer y liberar. El código siguiente es solo un ejemplo de la forma del contrato, no una dependencia concreta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fixture_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup-ci-1842-7f3a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup-ci-1842-7f3a@example.test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-05T04:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reserved"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;reserve&lt;/code&gt; debe ser idempotente para el mismo &lt;code&gt;run_id&lt;/code&gt;: si el job se reintenta, devuelve el fixture existente en lugar de crear un segundo recurso. &lt;code&gt;read&lt;/code&gt; debería aceptar un límite de tiempo y devolver una respuesta explícita cuando no hay mensaje. &lt;code&gt;release&lt;/code&gt; tiene que poder ejecutarse dos veces sin romperse, porque los hooks de limpieza no siempre corren en el orden esperado.&lt;/p&gt;

&lt;p&gt;Guarda el estado de la reserva, no el contenido completo del mensaje por defecto. Si necesitas conservar el cuerpo para investigar un fallo, redáctalo y asigna una retención corta. Esa frontera es parecida a usar &lt;a href="https://dev.to/silviutech/llms-contratos-minimos-para-revisar-prompts"&gt;contratos mínimos para revisar prompts&lt;/a&gt;: cada componente debe declarar qué recibe, qué devuelve y qué no debe filtrar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limpieza, reintentos y privacidad
&lt;/h2&gt;

&lt;p&gt;La limpieza debe ocurrir en dos lugares: al terminar normalmente y mediante un proceso de expiración. El primer camino mantiene el entorno ordenado; el segundo cubre cancelaciones, timeouts y agentes que se quedan sin conexión. Una métrica útil es la edad de los fixtures activos. Si sube cada semana, el problema no es solo de testing, es de operación.&lt;/p&gt;

&lt;p&gt;Los reintentos también necesitan límites. Reintenta una consulta cuando el mensaje todavía puede estar en tránsito, pero no repitas una operación de negocio que ya creó una cuenta sin comprobar su idempotencia. Conserva el primer error y marca el segundo intento como recuperación; así el panel no convierte una prueba inestable en una prueba saludable.&lt;/p&gt;

&lt;p&gt;Para los equipos que ejecutan pruebas en Kubernetes, &lt;a href="https://dev.to/alexcarteruk/kubernetes-rehearsa-tus-correos-de-guardia-56i9"&gt;un runbook para correos de guardia&lt;/a&gt; puede complementar este diseño: el fixture debe tener un propietario claro cuando el problema ocurre fuera del horario normal.&lt;/p&gt;

&lt;p&gt;Hay tambien una regla sencilla de privacidad: no pongas códigos de verificación, tokens ni contraseñas en los logs de CI. Guarda el &lt;code&gt;fixture_id&lt;/code&gt;, el asunto, los tiempos y un enlace interno a los artefactos protegidos. Un log pequeño pero útil vale más que copiar una bandeja entera.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Necesito un buzón físico para cada test?
&lt;/h3&gt;

&lt;p&gt;No. Puedes usar un proveedor controlado con destinatarios únicos y una consulta filtrada por ejecución. La condición es que dos tests no puedan leer el mismo mensaje por accidente.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué pasa si el email tarda demasiado?
&lt;/h3&gt;

&lt;p&gt;Registra el tiempo de cada consulta y termina con un diagnóstico claro. Un timeout debe decir si nunca llegó el mensaje, si llegó a otro destinatario o si la prueba perdió su fixture. Aumentar el timeout sin esa información solo oculta el problema.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Es seguro usar un servicio de correo temporal en producción?
&lt;/h3&gt;

&lt;p&gt;No para cuentas reales, recuperación de acceso o información sensible. Este patrón está pensado para staging, demos y pruebas aisladas. En producción, usa proveedores y políticas de retención aprobados por tu equipo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist para el siguiente sprint
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Genera un &lt;code&gt;run_id&lt;/code&gt; único antes de crear datos de prueba.&lt;/li&gt;
&lt;li&gt;Asocia cada dirección a un &lt;code&gt;fixture_id&lt;/code&gt; y a un propietario.&lt;/li&gt;
&lt;li&gt;Filtra los mensajes por destinatario exacto, no solo por asunto.&lt;/li&gt;
&lt;li&gt;Haz idempotentes &lt;code&gt;reserve&lt;/code&gt; y &lt;code&gt;release&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Añade expiración para cubrir jobs cancelados.&lt;/li&gt;
&lt;li&gt;Conserva un recibo redacted de cada fallo, sin tokens.&lt;/li&gt;
&lt;li&gt;Mide cuántos fixtures siguen vivos después de cada ejecución.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El cambio no requiere rehacer todo el sistema de emails. Empieza con un solo flujo de registro y observa si cada ejecución puede explicar qué dirección usó, qué mensaje leyó y cuándo liberó sus datos. Cuando esa respuesta sea clara, tu SaaS tendrá pruebas más reproducibles y un backend mucho menos misterioso.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Backend de SaaS: emails de prueba sin caos</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Fri, 02 Oct 2026 05:23:42 +0000</pubDate>
      <link>https://dev.to/hannahdev56/backend-de-saas-emails-de-prueba-sin-caos-kkn</link>
      <guid>https://dev.to/hannahdev56/backend-de-saas-emails-de-prueba-sin-caos-kkn</guid>
      <description>&lt;p&gt;Cuando construyes un SaaS, el registro parece una pantalla pequeña. En realidad activa una cadena completa: validación, email de confirmación, reintentos, métricas y, a veces, una tarea en segundo plano. Durante las primeras semanas yo solía probar todo con direcciones inventadas a mano. Era rapido, pero cada prueba dejaba datos que luego parecían usuarios reales.&lt;/p&gt;

&lt;p&gt;El problema no es solo elegir un fake email generator. El reto es definir qué significa un email de prueba dentro del backend y cómo se limpia después. Con un contrato simple, el equipo puede probar más rápido sin llenar el producto de cuentas fantasma.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: el email de prueba termina pareciendo un usuario real
&lt;/h2&gt;

&lt;p&gt;Un registro de prueba puede entrar en varias tablas y sistemas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;el usuario aparece en la base de datos;&lt;/li&gt;
&lt;li&gt;el evento &lt;code&gt;signup_started&lt;/code&gt; aumenta el embudo;&lt;/li&gt;
&lt;li&gt;el proveedor de email registra un envío;&lt;/li&gt;
&lt;li&gt;un job intenta leer el mensaje y confirmar la cuenta;&lt;/li&gt;
&lt;li&gt;una herramienta de marketing puede recibir el contacto.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si cada desarrollador decide por su cuenta qué dirección usar, el comportamiento deja de ser reproducible. Una prueba que funcionó ayer puede fallar hoy porque existe una cuenta con el mismo correo. Y cuando miras las métricas, ya no sabes si el aumento vino de clientes o de cinco sesiones de desarrollo.&lt;/p&gt;

&lt;p&gt;Para empezar, adopta un prefijo visible y un identificador de ejecución:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;saas-test+signup-20261002-001@example.test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El dominio reservado comunica que no es un contacto comercial. El sufijo permite buscar la prueba sin adivinar cuándo se creó.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define un contrato pequeño para cada fixture
&lt;/h2&gt;

&lt;p&gt;Una fixture no tiene que ser una plataforma compleja. Para cada ejecución guarda solo los datos que ayudan a repetirla:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup-20261002-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"saas-test+signup-20261002-001@example.test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"purpose"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-02T13:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"owner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"local-dev"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;run_id&lt;/code&gt; debe viajar en los logs, en el asunto del email y en el resultado del test. Así puedes distinguir un mensaje viejo de uno que pertenece a la prueba actual. Es una mejora pequeña y bastante util cuando varias personas trabajan en el mismo staging.&lt;/p&gt;

&lt;p&gt;También conviene registrar la razón de cada email. &lt;code&gt;signup&lt;/code&gt;, &lt;code&gt;password-reset&lt;/code&gt; y &lt;code&gt;invite&lt;/code&gt; son propósitos diferentes, aunque todos reciban un enlace. Una búsqueda como &lt;code&gt;tamp mail com&lt;/code&gt; puede servir para descubrir cómo piensa un usuario, pero no sustituye este contrato técnico.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separa desarrollo, staging y métricas
&lt;/h2&gt;

&lt;p&gt;El backend debe saber en qué entorno está ejecutando el flujo. En local, puedes usar un buzón simulado o una bandeja de prueba. En staging, necesitas un servicio controlado y reglas para borrar los datos. En producción, las direcciones de prueba deben estar bloqueadas o aisladas por una política explicita.&lt;/p&gt;

&lt;p&gt;Una regla útil es que ningún fixture de desarrollo pueda activar una campaña. Marca el registro con &lt;code&gt;is_test=true&lt;/code&gt; y haz que ese campo sea visible para las consultas de analítica. No dependas solo del dominio: alguien puede copiar una dirección de prueba o importar datos sin querer.&lt;/p&gt;

&lt;p&gt;Cuando un equipo crece, vale la pena documentar también las excepciones. Por ejemplo, una verificación de entrega puede necesitar un inbox real, mientras que un test de validación solo necesita revisar que el job se encoló. No mandes emails reales si el caso no lo requiere.&lt;/p&gt;

&lt;p&gt;Para alertas y fallos, ayuda pensar en los &lt;a href="https://dev.to/alexcarteruk/sre-correos-de-incidentes-sin-ruido-operativo-5bc6"&gt;correos de incidentes sin ruido operativo&lt;/a&gt;: el evento de prueba debe ser fácil de filtrar y no competir con la alerta de un cliente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Haz idempotente el flujo de verificación
&lt;/h2&gt;

&lt;p&gt;Los reintentos son normales. Un usuario puede pulsar dos veces el botón, el worker puede perder la respuesta o el proveedor puede entregar el mismo evento más de una vez. Si cada intento crea otra cuenta o manda otro email, las pruebas se vuelven caras y confusas.&lt;/p&gt;

&lt;p&gt;Usa una clave de idempotencia basada en el propósito, el usuario y la ejecución:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signup:{user_id}:{run_id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Antes de crear un nuevo mensaje, consulta esa clave. Si ya existe un resultado válido, devuelve el mismo estado. Si expiró, permite un nuevo intento con un número diferente, pero conserva la relación con el &lt;code&gt;run_id&lt;/code&gt; original.&lt;/p&gt;

&lt;p&gt;En la práctica, esto hace que el endpoint sea más facil de probar y también más seguro para clientes reales. Las pruebas de interfaz pueden cubrir los estados pendiente, expirado y confirmado; un ejemplo de &lt;a href="https://dev.to/pak3ng/react-signup-accesible-para-pruebas-reales-4ijd"&gt;signup accesible para pruebas reales&lt;/a&gt; ayuda a conectar ese contrato del backend con la experiencia visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist para el primer MVP
&lt;/h2&gt;

&lt;p&gt;Antes de añadir más infraestructura, revisa estos puntos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cada fixture tiene un &lt;code&gt;run_id&lt;/code&gt; único y una fecha de expiración.&lt;/li&gt;
&lt;li&gt;El entorno marca los datos de prueba con &lt;code&gt;is_test=true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Los logs incluyen el propósito y la clave de idempotencia.&lt;/li&gt;
&lt;li&gt;Las métricas excluyen los registros de prueba por defecto.&lt;/li&gt;
&lt;li&gt;El worker puede distinguir un mensaje nuevo de uno antiguo.&lt;/li&gt;
&lt;li&gt;Hay un job de limpieza que no borra datos de clientes.&lt;/li&gt;
&lt;li&gt;El equipo sabe cómo reproducir una ejecución fallida.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Un enlace contextual a &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;tempmailso&lt;/a&gt; puede ser útil cuando necesitas una bandeja temporal para comprobar un flujo manual. Mantén ese uso separado de la automatización crítica y nunca pongas datos de clientes en una bandeja compartida.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resumen
&lt;/h2&gt;

&lt;p&gt;Un fake email generator resuelve solo la creación de una dirección. Un backend de SaaS confiable necesita algo más: identidad de ejecución, expiración, separación por entorno y reintentos idempotentes. Empieza con un contrato JSON pequeño, etiqueta los datos de prueba y mide la limpieza como parte del flujo.&lt;/p&gt;

&lt;p&gt;No hace falta construir una plataforma enorme para lograrlo. Con estas reglas, las pruebas son mas faciles de repetir, los dashboards cuentan una historia más honesta y el equipo puede avanzar sin miedo a romper los datos de clientes.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>SaaS: diseña emails de prueba como un contrato</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Wed, 23 Sep 2026 14:23:23 +0000</pubDate>
      <link>https://dev.to/hannahdev56/saas-disena-emails-de-prueba-como-un-contrato-ke</link>
      <guid>https://dev.to/hannahdev56/saas-disena-emails-de-prueba-como-un-contrato-ke</guid>
      <description>&lt;p&gt;Un sistema de emails de prueba parece una pieza pequeña de un SaaS, hasta que una prueba falla y nadie sabe si el problema está en el formulario, la cola o el buzón. La solución no es solamente generar más direcciones: es definir un contrato sencillo que todos los componentes puedan entender.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué un email de prueba necesita un contrato
&lt;/h2&gt;

&lt;p&gt;En un proyecto nuevo solemos escribir una prueba así: crear usuario, esperar un correo y hacer clic en el enlace. Funciona durante unos días. Luego llegan las pruebas en paralelo, los reintentos y los mensajes tardíos. El resultado son tests inestables y un equipo que repite el mismo diagnóstico cada mañana.&lt;/p&gt;

&lt;p&gt;Un contrato de email describe qué se envía, a quién, cuándo puede leerse y cómo se identifica. No es una especificación enorme. Es un acuerdo pequeño entre la aplicación, el proveedor de correo de pruebas y la suite de CI.&lt;/p&gt;

&lt;p&gt;También ayuda a separar el correo de producción de los datos temporales. Para una prueba local puede ser suficiente una &lt;a href="https://tempmailso.com/" rel="noopener noreferrer"&gt;dirección de correo desechable&lt;/a&gt;, pero la regla importante es que la dirección tenga un dueño y una vida útil clara.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los cuatro estados mínimos
&lt;/h2&gt;

&lt;p&gt;Para empezar, modela el flujo con cuatro estados:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Creado:&lt;/strong&gt; la prueba reservó un buzón y guardó su identificador.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Esperando:&lt;/strong&gt; la aplicación pidió el correo, pero todavía no ha llegado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recibido:&lt;/strong&gt; existe un mensaje que coincide con el asunto, el destinatario y el intento.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumido o expirado:&lt;/strong&gt; la prueba terminó, o el buzón ya no debe aceptar lecturas.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El estado &lt;code&gt;recibido&lt;/code&gt; no debería significar simplemente “hay un email”. Debe incluir un &lt;code&gt;correlation_id&lt;/code&gt;, el tipo de mensaje y la hora de recepción. Con esos datos, un fallo deja una pista útil en vez de un “timeout” genérico.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseña el contrato paso a paso
&lt;/h2&gt;

&lt;p&gt;Un contrato práctico puede ser un objeto JSON parecido a este:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"test_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup-4821"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signup-4821@example.test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email_verification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"correlation_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"run-2026-09-23-4821"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-23T15:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"waiting"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Primero genera &lt;code&gt;test_id&lt;/code&gt; y &lt;code&gt;correlation_id&lt;/code&gt; antes de abrir el formulario. Segundo, pasa esos valores a la aplicación o inclúyelos en el contexto que el sistema de email pueda devolver. Tercero, busca por varios campos: destinatario exacto, tipo de mensaje y correlación. Buscar solo por el asunto es una invitación a leer el mensaje equivocado.&lt;/p&gt;

&lt;p&gt;Finalmente, define un timeout y un error útil. “No llegó el email” es poco accionable. “No llegó &lt;code&gt;email_verification&lt;/code&gt; para &lt;code&gt;signup-4821&lt;/code&gt; después de 45 segundos; último evento: queued” permite revisar la cola, el worker o el proveedor correcto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aislamiento y trazabilidad en el backend
&lt;/h2&gt;

&lt;p&gt;Cada ejecución paralela necesita un buzón, alias o namespace que no pueda confundirse con otro. Un contador global no basta: dos workers pueden solicitarlo al mismo tiempo. Usa un identificador aleatorio corto y conserva el vínculo entre ejecución, usuario de prueba y mensaje.&lt;/p&gt;

&lt;p&gt;Guarda en los artefactos de CI solo lo necesario: estado, identificadores, timestamps y un resumen del mensaje. No copies tokens de recuperación ni el contenido completo si no es imprescindible. Esta decisión de privacidad es parte del diseño de Backend, no una tarea para después.&lt;/p&gt;

&lt;p&gt;Cuando una prueba falle, registra una secuencia de eventos: &lt;code&gt;created&lt;/code&gt;, &lt;code&gt;queued&lt;/code&gt;, &lt;code&gt;delivered&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt; y &lt;code&gt;expired&lt;/code&gt;. Puedes complementar el flujo con &lt;a href="https://dev.to/alexcarteruk/eks-alertas-de-cambios-con-contexto-real-2iji"&gt;alertas de cambios con contexto real&lt;/a&gt; y con &lt;a href="https://dev.to/alexcarteruk/kubernetes-correos-utiles-para-alertas-nocturnas-pan"&gt;correos útiles para alertas nocturnas&lt;/a&gt;, sobre todo si tu CI comparte infraestructura con servicios de operación.&lt;/p&gt;

&lt;p&gt;Una nota curiosa: al buscar documentación o reportar un incidente alguien puede escribir “fake e mail com” o “temp mailid”. No son nombres de campos válidos, pero conviene reconocerlos en la búsqueda interna para encontrar el contexto sin meterlos en el código.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes que parecen pequeños
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reutilizar buzones:&lt;/strong&gt; un mensaje viejo puede hacer que la prueba pase por accidente.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Esperar sin límite:&lt;/strong&gt; un bucle infinito bloquea workers y oculta la causa real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No fijar el tipo de mensaje:&lt;/strong&gt; un correo de bienvenida puede confundirse con el de verificación.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Borrar los artefactos demasiado pronto:&lt;/strong&gt; después nadie puede reconstruir el fallo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mezclar producción y pruebas:&lt;/strong&gt; es un riesgo de privacidad y también una fuente de datos contaminados.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confiar solo en el asunto:&lt;/strong&gt; cambios de idioma o plantillas pueden romper el selector.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En SaaS, la tentación es añadir reintentos hasta que el test pase. Los reintentos ayudan con fallos transitorios, pero no corrigen un contrato incompleto. Mantén pocos intentos, registra cada uno y deja que el error final explique que ocurrió.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist de cierre
&lt;/h2&gt;

&lt;p&gt;Antes de dar por terminado el flujo, comprueba:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cada prueba crea un identificador único.&lt;/li&gt;
&lt;li&gt;El contrato define destinatario, tipo, correlación, expiración y estado.&lt;/li&gt;
&lt;li&gt;La lectura filtra por más de un campo.&lt;/li&gt;
&lt;li&gt;Los tests paralelos no comparten datos.&lt;/li&gt;
&lt;li&gt;El timeout devuelve el último evento conocido.&lt;/li&gt;
&lt;li&gt;Los artefactos no contienen secretos ni tokens reutilizables.&lt;/li&gt;
&lt;li&gt;La limpieza ocurre después de guardar el resumen del resultado.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La idea central es simple: un email de prueba no es un detalle de la UI, sino una dependencia del backend con estados y límites. Cuando lo tratas como un contrato, las pruebas de tu SaaS se vuelven más repetibles, los fallos son más fáciles de investigar y el equipo pierde menos tiempo adivinando.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>SaaS: un contrato simple para emails de prueba</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Tue, 22 Sep 2026 20:24:07 +0000</pubDate>
      <link>https://dev.to/hannahdev56/saas-un-contrato-simple-para-emails-de-prueba-2cie</link>
      <guid>https://dev.to/hannahdev56/saas-un-contrato-simple-para-emails-de-prueba-2cie</guid>
      <description>&lt;p&gt;Cuando un SaaS empieza a crecer, el email de bienvenida deja de ser un detalle del formulario. Puede activar una cuenta, iniciar una prueba, confirmar un dominio o entregar el acceso a otra persona. Si cada prueba espera que "llegue algo" sin un contrato claro, el equipo termina depurando síntomas: un test intermitente, un usuario duplicado o una bandeja con mensajes que nadie sabe a quién pertenecen.&lt;/p&gt;

&lt;p&gt;Una forma sencilla de ordenar este problema es crear una fixture de email: una pequeña pieza de infraestructura que crea una dirección, espera mensajes y devuelve solo la información que la prueba necesita. En este artículo veremos un flujo para un equipo de SaaS pequeño, con palabras simples y decisiones que se pueden aplicar en un backend real.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: el email no es solo un detalle del formulario
&lt;/h2&gt;

&lt;p&gt;Imagina un onboarding con cuatro pasos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El usuario escribe su email.&lt;/li&gt;
&lt;li&gt;El backend crea una cuenta pendiente.&lt;/li&gt;
&lt;li&gt;Se envía un enlace de verificación.&lt;/li&gt;
&lt;li&gt;El usuario abre el enlace y pasa a estado activo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El navegador puede hacer clic correctamente y aun así la prueba fallar. El mensaje puede estar retrasado, puede pertenecer a otro test paralelo o puede contener un enlace viejo. Un &lt;code&gt;sleep(5000)&lt;/code&gt; solo oculta la causa y hace que la suite sea mas lenta.&lt;/p&gt;

&lt;p&gt;La solución no es pedir que el correo llegue instantaneamente. Es acordar qué significa que el mensaje sea el correcto: destinatario, intención, identificador de ejecución y una ventana de tiempo limitada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define el contrato antes de escribir código
&lt;/h2&gt;

&lt;p&gt;Un contrato mínimo para una fixture de correo de usar y tirar puede tener estos campos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;address&lt;/code&gt;: dirección única para la ejecución.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;runId&lt;/code&gt;: identificador corto de la prueba.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;subject&lt;/code&gt;: intención esperada, por ejemplo &lt;code&gt;Confirma tu cuenta&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;receivedAfter&lt;/code&gt;: instante desde el que aceptamos un mensaje.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;link&lt;/code&gt;: enlace que la prueba abrirá, sin guardar el cuerpo entero en los logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El &lt;code&gt;runId&lt;/code&gt; debe viajar en la dirección, en el asunto o en metadatos disponibles para el adaptador. No conviene buscar "el mensaje más reciente" de un buzón compartido, porque dos reintentos pueden intercambiar sus resultados. Ese error parece raro hasta que el CI ejecuta muchos workers a la vez.&lt;/p&gt;

&lt;p&gt;También conviene separar dos responsabilidades. La fixture sabe consultar el buzón. El test sabe qué comportamiento del producto debe comprobar. Así, cambiar de proveedor no obliga a reescribir todos los casos de onboarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una fixture pequeña para el backend
&lt;/h2&gt;

&lt;p&gt;Podemos esconder el proveedor detrás de una interfaz pequeña. El ejemplo siguiente no depende de una API concreta; muestra el límite que necesitamos para probar el flujo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TestMail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;verificationUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Mailbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;createAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;listMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TestMail&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Mailbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TestMail&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipient&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verificationUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://app.example.com/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Verification email not received for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En producción, el adaptador debe escapar los datos del proveedor y aplicar límites. No guardes el contenido completo del mensaje en cada log: basta con el ID, el destinatario enmascarado, el asunto y el resultado de la validación. Para entender cómo un feedback estable al enviar emails ayuda a la interfaz, también puedes revisar &lt;a href="https://dev.to/silviutech/react-feedback-estable-al-enviar-emails-3gda"&gt;este patrón de feedback estable al enviar emails&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo usarla en un flujo de onboarding
&lt;/h2&gt;

&lt;p&gt;El caso de prueba queda mas cercano a la intención del usuario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`signup-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;signupPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;signupPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitForVerification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verificationUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accountPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toHaveText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Activa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para un entorno de desarrollo, una &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;dirección de correo desechable&lt;/a&gt; puede servir para separar datos de prueba de una bandeja personal. Aun así, el producto no debería depender de que una persona recuerde borrar mensajes. Define retención corta, no uses datos reales y evita poner tokens en reportes públicos.&lt;/p&gt;

&lt;p&gt;La misma idea sirve para operaciones. Si un servicio envía emails después de un despliegue, la prueba debe identificar qué ejecución originó la alerta. Para ampliar el ejemplo hacia operaciones, es útil &lt;a href="https://dev.to/alexcarteruk/kubernetes-correos-de-rollback-sin-confusion-bk4"&gt;probar correos de mantenimiento en Kubernetes&lt;/a&gt; con la misma disciplina de ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes y una lista de comprobación
&lt;/h2&gt;

&lt;p&gt;Estos fallos aparecen mucho cuando se crea la primera versión:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compartir una dirección entre workers y confiar en el orden de llegada.&lt;/li&gt;
&lt;li&gt;Usar el asunto como único filtro, sin destinatario ni &lt;code&gt;runId&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Esperar para siempre cuando el proveedor no responde.&lt;/li&gt;
&lt;li&gt;Hacer que el test conozca cada detalle de la API externa.&lt;/li&gt;
&lt;li&gt;Registrar enlaces completos, aunque contengan un token de acceso.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Una revisión rapida puede preguntar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Cada ejecución tiene identidad propia?&lt;/li&gt;
&lt;li&gt;¿La espera tiene un límite y un intervalo razonable?&lt;/li&gt;
&lt;li&gt;¿El fallo deja evidencia util, no solo un timeout?&lt;/li&gt;
&lt;li&gt;¿El adaptador puede reemplazarse sin tocar el onboarding?&lt;/li&gt;
&lt;li&gt;¿Los mensajes y tokens se eliminan o expiran?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;También conviene probar el caso negativo: un mensaje con el destinatario correcto pero con otro &lt;code&gt;runId&lt;/code&gt; no debe activar la cuenta. Es una comprobación pequeña y evita errores que son dificiles de ver en una demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas y respuestas rápidas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Necesito un proveedor externo?
&lt;/h3&gt;

&lt;p&gt;No siempre. Para una unidad de backend puedes usar un adaptador en memoria. Para una prueba de extremo a extremo necesitas una bandeja aislada o un servicio de pruebas que permita consultar mensajes de forma controlada.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué hago si el mensaje tarda demasiado?
&lt;/h3&gt;

&lt;p&gt;Guarda un recibo con &lt;code&gt;runId&lt;/code&gt;, destinatario enmascarado, intentos y tiempo transcurrido. Eso permite distinguir un bug de la aplicación de una demora del proveedor, en vez de aumentar el timeout a ciegas.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Debo aceptar cualquier enlace de verificación?
&lt;/h3&gt;

&lt;p&gt;No. Comprueba el origen, el entorno y la relación con la cuenta creada. Un email recibido no demuestra por sí solo que el enlace sea el esperado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recapitulación
&lt;/h2&gt;

&lt;p&gt;Una fixture de email no necesita ser un sistema enorme. Con una identidad por ejecución, una consulta acotada y un contrato claro, el onboarding de un SaaS se vuelve mas fácil de probar. Empieza con un solo flujo, mide qué evidencia necesitas cuando falla y luego comparte el adaptador con el resto del backend. Ese pequeño paso suele ahorrar bastante ruido cuando el producto y el equipo crecen.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>SaaS: convierte eventos de marketing en decisiones útiles</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Thu, 17 Sep 2026 08:23:03 +0000</pubDate>
      <link>https://dev.to/hannahdev56/saas-convierte-eventos-de-marketing-en-decisiones-utiles-3aan</link>
      <guid>https://dev.to/hannahdev56/saas-convierte-eventos-de-marketing-en-decisiones-utiles-3aan</guid>
      <description>&lt;p&gt;Cuando construyes un SaaS pequeño, es tentador registrar todo: cada clic, cada cambio de pantalla y cada intento de correo. Luego llega el lunes y tienes cientos de eventos, pero no sabes qué decisión tomar. A mi me pasó durante una experimento de onboarding: el panel se veía muy completo, pero no explicaba porque algunos usuarios no llegaban al primer valor.&lt;/p&gt;

&lt;p&gt;La solución no fue comprar otra herramienta. Fue conectar cada evento con una pregunta de marketing o de producto. Este proceso es bastante alcanzable, incluso si el equipo son dos personas.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: demasiados eventos, pocas decisiones
&lt;/h2&gt;

&lt;p&gt;Marketing suele preguntar qué campaña trae usuarios que activan el producto. Producto pregunta dónde se atascan. Backend solo quiere que el sistema no falle. Si cada equipo nombra los eventos de una manera, el embudo se vuelve una conversación de opiniones.&lt;/p&gt;

&lt;p&gt;Un evento útil debe responder, como mínimo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Qué ocurrió?&lt;/li&gt;
&lt;li&gt;¿A qué cuenta o usuario pertenece?&lt;/li&gt;
&lt;li&gt;¿En qué momento ocurrió?&lt;/li&gt;
&lt;li&gt;¿Qué versión del producto lo produjo?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No necesitas capturar datos privados para responder esto. En muchos casos basta con un identificador interno, una fuente de campaña y un nombre de etapa.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define una pregunta de negocio
&lt;/h2&gt;

&lt;p&gt;Empieza con una sola pregunta, por ejemplo: “¿Qué porcentaje de los registros de una campaña crea su primer proyecto en 24 horas?”. Es una pregunta mejor que “¿cuántas personas visitaron la pantalla?”.&lt;/p&gt;

&lt;p&gt;Después escribe la definición de activación. Para este ejemplo, podría ser &lt;code&gt;project_created&lt;/code&gt; dentro de las primeras 24 horas después de &lt;code&gt;signup_completed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;La definición parece simple, pero evita que el equipo cambie la métrica cada semana. También ayuda a explicar el resultado a alguien que no mira código.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Diseña un contrato pequeño de eventos
&lt;/h2&gt;

&lt;p&gt;Un contrato inicial puede verse así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"project_created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usr_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"occurred_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-17T08:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"newsletter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schema_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El campo &lt;code&gt;source&lt;/code&gt; conecta el marketing con el comportamiento real. &lt;code&gt;schema_version&lt;/code&gt; evita que un cambio futuro rompa las consultas antiguas. Conviene documentar los eventos en un archivo sencillo, aunque al principio sea solo un Markdown.&lt;/p&gt;

&lt;p&gt;Para la interfaz, también importa &lt;a href="https://dev.to/silviutech/react-ayuda-de-email-sin-mover-el-layout"&gt;mantener estable la experiencia del formulario&lt;/a&gt;. Si el botón salta o el mensaje de error aparece tarde, puedes atribuir una caída a la campaña cuando en realidad el problema es la experiencia.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Separa adquisición y activación
&lt;/h2&gt;

&lt;p&gt;No mezcles “vino de una campaña” con “encontró valor”. Una campaña puede producir muchos registros y pocos usuarios activos. Otra puede traer menos visitas, pero mejores cuentas.&lt;/p&gt;

&lt;p&gt;Guarda estas etapas por separado:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;landing_viewed&lt;/code&gt;: la persona llegó.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;signup_started&lt;/code&gt;: comenzó el registro.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;signup_completed&lt;/code&gt;: terminó el registro.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;first_value_reached&lt;/code&gt;: realizó la acción principal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Así puedes localizar el corte. Si muchas personas abandonan entre el segundo y tercer paso, revisa el formulario. Si terminan el registro pero no crean un proyecto, revisa el primer recorrido dentro del producto.&lt;/p&gt;

&lt;p&gt;Una dirección de prueba o un &lt;code&gt;tem email&lt;/code&gt; puede ser útil durante QA, pero no debe contaminar tus métricas de adquisición. Marca los datos de prueba con &lt;code&gt;environment: "test"&lt;/code&gt; y exclúyelos desde la consulta, no manualmente al final.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Haz que los datos sobrevivan a los errores
&lt;/h2&gt;

&lt;p&gt;El seguimiento nunca debe bloquear la acción principal. Si el proveedor de analítica está lento, el usuario todavía debe poder crear su proyecto. En backend, coloca los eventos en una cola o usa un registro local breve para reintentar.&lt;/p&gt;

&lt;p&gt;También conviene hacer las operaciones idempotentes. Si un reintento envía dos veces &lt;code&gt;first_value_reached&lt;/code&gt;, el panel puede mostrar una activación falsa. Una clave como &lt;code&gt;user_id + event_name + day&lt;/code&gt; puede servir para deduplicar, siempre que coincida con la definición del evento.&lt;/p&gt;

&lt;p&gt;En frontend, prueba también los estados de carga y error; &lt;a href="https://dev.to/silviutech/react-prueba-emails-sin-romper-estados-accesibles-48a9"&gt;probar los estados del formulario con accesibilidad&lt;/a&gt; descubre problemas que las métricas solas no explican.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Revisa el embudo cada semana
&lt;/h2&gt;

&lt;p&gt;Una revisión útil dura 20 minutos. Mira el volumen por etapa, compara dos fuentes de campaña y abre algunos ejemplos reales. Pregunta: “¿Qué haríamos distinto después de ver este número?”. Si la respuesta es nada, la métrica no necesita estar en el panel principal.&lt;/p&gt;

&lt;p&gt;No optimices solo el porcentaje. Un cambio puede mejorar la conversión y atraer cuentas de peor calidad. Añade una señal sencilla de salud, como proyectos creados o usuarios que regresan, para no celebrar una victoria incompleta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Nombrar el mismo evento de tres formas.&lt;/li&gt;
&lt;li&gt;Guardar UTM en un campo que nadie documentó.&lt;/li&gt;
&lt;li&gt;Contar usuarios de prueba como clientes potenciales.&lt;/li&gt;
&lt;li&gt;Hacer que una llamada de analítica bloquee el registro.&lt;/li&gt;
&lt;li&gt;Cambiar la definición de activación sin anotar la fecha.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recapitulación
&lt;/h2&gt;

&lt;p&gt;Un SaaS no necesita un sistema de analítica enorme para aprender. Elige una pregunta, define cuatro etapas, crea un contrato pequeño y separa los datos de prueba. Después conecta las campañas con la activación, protege el flujo frente a fallos y revisa solo métricas que cambien una decisión.&lt;/p&gt;

&lt;p&gt;El resultado es menos ruido y mejores conversaciones entre marketing, producto y backend. Y eso, para un equipo pequeño, vale más que otro panel lleno de gráficos.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>marketing</category>
      <category>backend</category>
      <category>productivity</category>
    </item>
    <item>
      <title>SaaS: emails de prueba que no rompen tus métricas</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Wed, 16 Sep 2026 08:23:11 +0000</pubDate>
      <link>https://dev.to/hannahdev56/saas-emails-de-prueba-que-no-rompen-tus-metricas-3foc</link>
      <guid>https://dev.to/hannahdev56/saas-emails-de-prueba-que-no-rompen-tus-metricas-3foc</guid>
      <description>&lt;p&gt;Cuando construyes un SaaS, probar el email de bienvenida parece una tarea pequeña. Creas una cuenta, esperas el mensaje y haces clic en el enlace. El problema llega cuando ese mismo usuario de prueba aparece en tus métricas de activación, abre campañas de marketing y deja datos que luego nadie sabe si son reales.&lt;/p&gt;

&lt;p&gt;Me ha pasado varias veces: una prueba manual funciona, pero el panel dice que la conversión subió de forma extraña. Al revisar, había cuentas creadas por el equipo, correos repetidos y algún registro con &lt;code&gt;temp gamil com&lt;/code&gt; escrito en una nota. No era un problema de marketing. Era un problema de identidad de prueba.&lt;/p&gt;

&lt;p&gt;En este artículo comparto un flujo práctico para que tus pruebas sean repetibles sin convertir el producto en un laboratorio confuso.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema de probar con correos reales
&lt;/h2&gt;

&lt;p&gt;Usar tu correo personal para cada registro tiene tres riesgos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mezclas mensajes de producción con pruebas;&lt;/li&gt;
&lt;li&gt;puedes activar varias veces el mismo recorrido de onboarding;&lt;/li&gt;
&lt;li&gt;los eventos de test contaminan los informes de producto.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un disposable email address generator o un temporary email generator puede ayudarte a obtener una dirección aislada para una sesión. Pero la dirección, por sí sola, no resuelve todo. También necesitas marcar la cuenta, registrar su propósito y decidir cuándo debe excluirse de los análisis.&lt;/p&gt;

&lt;p&gt;Por eso prefiero &lt;a href="https://dev.to/hannahdev56/como-probar-correos-de-onboarding-en-un-saas-sin-ensuciar-metricas-pjn"&gt;probar correos de onboarding&lt;/a&gt; con un contrato explícito, en vez de confiar en que alguien recordará borrar los datos después.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define una identidad de prueba
&lt;/h2&gt;

&lt;p&gt;Antes de abrir el navegador, genera un identificador único para la ejecución. Puede ser algo como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run_2026_09_16_0822
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guarda ese valor junto a la dirección de correo y envíalo al backend como metadato de test. En una aplicación pequeña, una tabla puede tener estas columnas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email | test_run_id | environment | created_at | is_test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El campo &lt;code&gt;is_test&lt;/code&gt; no debe depender solamente del dominio del correo. Un usuario de staging puede tener un correo normal, y una cuenta de producción puede haberse creado desde una prueba manual. El contexto de la ejecución es una señal mucho más útil.&lt;/p&gt;

&lt;p&gt;También conviene poner el identificador en el nombre de la prueba y en los logs. Asi, cuando un webhook llegue tarde, puedes relacionarlo con la cuenta correcta sin adivinar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separa eventos de producto y de test
&lt;/h2&gt;

&lt;p&gt;La solución más clara es mantener la separación en el momento de emitir eventos. Por ejemplo, el evento &lt;code&gt;onboarding_email_sent&lt;/code&gt; puede incluir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"u_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"test_run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"run_2026_09_16_0822"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"playwright"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Después, el pipeline de analítica puede excluir &lt;code&gt;is_test: true&lt;/code&gt; de los paneles de negocio, pero conservarlo en un panel de calidad. Así puedes saber si el email se envió, si el enlace llegó y cuánto tardó, sin contar esa actividad como activación real.&lt;/p&gt;

&lt;p&gt;Este diseño también sirve para una prueba hecha desde la interfaz. No hace falta construir un sistema enorme: una cabecera interna, una variable de entorno o un usuario de test conocido puede ser suficiente al principio. Lo importante es que la regla sea visible para todo el equipo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un flujo sencillo de backend
&lt;/h2&gt;

&lt;p&gt;Mi secuencia habitual es esta:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Crear un &lt;code&gt;test_run_id&lt;/code&gt; nuevo.&lt;/li&gt;
&lt;li&gt;Pedir una dirección de correo temporal para esa ejecución.&lt;/li&gt;
&lt;li&gt;Crear el usuario con &lt;code&gt;is_test: true&lt;/code&gt; en staging o en un entorno controlado.&lt;/li&gt;
&lt;li&gt;Esperar un mensaje posterior al momento de creación, no simplemente el último mensaje del buzón.&lt;/li&gt;
&lt;li&gt;Guardar asunto, timestamp, identificador del mensaje y resultado del clic.&lt;/li&gt;
&lt;li&gt;Eliminar o anonimizar los datos cuando termina la prueba.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Para evitar carreras, el consumidor de email debe filtrar por destinatario, asunto y una marca única. Revisar emails de activación es mucho más fácil cuando el test sabe exactamente qué mensaje está buscando.&lt;/p&gt;

&lt;p&gt;No guardes el contenido completo del correo si no lo necesitas. Un hash del mensaje, el enlace extraído y el estado de la aserción suelen bastar para depurar. Es una pequeña mejora de privacidad y reduce basura en la base de datos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes
&lt;/h2&gt;

&lt;p&gt;El primer error es usar una sola bandeja compartida para todas las pruebas. El segundo es limpiar la cuenta antes de guardar la evidencia. El tercero es excluir por completo los eventos de test y perder la posibilidad de medir la salud del sistema de email.&lt;/p&gt;

&lt;p&gt;Otro fallo frecuente: confiar en una dirección fija y asumir que siempre estará vacía. Los mensajes pueden llegar tarde o una ejecución paralela puede leerlos. Un identificador por ejecución hace el problema mucho mas manejable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist final
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;¿Cada ejecución tiene un identificador único?&lt;/li&gt;
&lt;li&gt;¿La cuenta está marcada como test desde el backend?&lt;/li&gt;
&lt;li&gt;¿Tus métricas de negocio excluyen eventos de prueba?&lt;/li&gt;
&lt;li&gt;¿Conservas evidencia mínima del envío y la recepción?&lt;/li&gt;
&lt;li&gt;¿El filtro de mensajes usa una marca temporal y un destinatario?&lt;/li&gt;
&lt;li&gt;¿Existe una política de limpieza para cuentas y buzones?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No necesitas una plataforma sofisticada para empezar. Un correo temporal aislado, una columna &lt;code&gt;is_test&lt;/code&gt; y unos logs consistentes ya cambian mucho la calidad del trabajo. Cuando el equipo puede distinguir una señal de producto de una señal de prueba, el SaaS crece con métricas más confiables y con menos discusiones sobre qué número es el bueno.&lt;/p&gt;

&lt;p&gt;Como siguiente paso, elige un solo recorrido de onboarding y añade este contrato. Después de una semana, revisa qué datos ayudaron realmente a diagnosticar fallos y ajusta el flujo. Esa iteración pequeña suele rendir más que añadir otra herramienta al stack.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>SaaS: mide el marketing desde el primer correo</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Tue, 15 Sep 2026 08:23:24 +0000</pubDate>
      <link>https://dev.to/hannahdev56/saas-mide-el-marketing-desde-el-primer-correo-2dfd</link>
      <guid>https://dev.to/hannahdev56/saas-mide-el-marketing-desde-el-primer-correo-2dfd</guid>
      <description>&lt;p&gt;Un onboarding de SaaS no empieza cuando alguien paga. Empieza cuando una persona deja su correo y espera que el producto le demuestre algo útil. En mis experimentos de producto, mirar ese primer correo como una señal de marketing ha sido más práctico que perseguir métricas vanidosas.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: medir tarde
&lt;/h2&gt;

&lt;p&gt;Muchos equipos miran solamente registros, activaciones y conversiones mensuales. Esas cifras sirven, pero llegan tarde. Si el correo de verificación no se entrega, tarda demasiado o el usuario no encuentra el siguiente paso, el problema ya está escondido dentro del embudo.&lt;/p&gt;

&lt;p&gt;La idea no es vigilar a cada usuario de forma invasiva. Es crear un pequeño contrato observable: solicitud creada, mensaje enviado, mensaje recibido y siguiente acción completada. Con cuatro eventos podemos conversar sobre el onboarding con datos, no solo con opiniones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una señal mínima y útil
&lt;/h2&gt;

&lt;p&gt;Para un SaaS pequeño, recomiendo guardar estos campos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event_name: verification_email_sent
account_id: 8f2...
occurred_at: 2026-09-15T08:00:00Z
delivery_provider: primary
request_id: req_123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No guardes el contenido del correo ni el endereço completo si no hace falta. Un identificador interno y un hash controlado suelen ser suficiente para depurar. En ambientes de prueba, un temporary email generator puede ayudar a validar el flujo, pero no debe convertirse en una métrica de usuarios reales.&lt;/p&gt;

&lt;p&gt;También conviene separar &lt;code&gt;sent&lt;/code&gt; de &lt;code&gt;opened&lt;/code&gt;. Un proveedor puede confirmar que aceptó el mensaje, pero eso no prueba que llegó a la bandeja ni que la persona entendió la propuesta de valor. Son señales distintas, y mezclarlas produce decisiones medio raras.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementación paso a paso
&lt;/h2&gt;

&lt;p&gt;Primero, define una sola función para registrar eventos. Después, añade un &lt;code&gt;request_id&lt;/code&gt; que viaje desde la petición HTTP hasta la cola y el proveedor. Por ultimo, crea un panel pequeño con tres preguntas: ¿cuántos correos fallan?, ¿cuánto tarda el siguiente paso?, ¿qué porcentaje abandona?&lt;/p&gt;

&lt;p&gt;Para probar el backend, puedes combinar pruebas de integración con &lt;a href="https://dev.to/alexcarteruk/como-probar-correos-de-handoff-en-guardias-sre-m14"&gt;probar correos de handoff&lt;/a&gt; y &lt;a href="https://dev.to/silviutech/fastapi-pruebas-limpias-para-email-de-facebook-469k"&gt;pruebas limpias de email en FastAPI&lt;/a&gt;. Un &lt;code&gt;dummy e mail&lt;/code&gt; puede servir como dato de test, pero etiquétalo claramente para que no contamine los informes.&lt;/p&gt;

&lt;p&gt;En marketing, conecta el evento con una acción de producto, no con una campaña imaginaria. Por ejemplo, si una persona verifica y crea su primer proyecto en diez minutos, esa secuencia te dice más que contar solamente clicks. A veces un dashboard muy grande solo oculta que nadie sabe que decisión tomar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes
&lt;/h2&gt;

&lt;p&gt;El primer error es medir cada cosa desde el día uno. Mantén el esquema chico; luego amplia cuando aparezca una pregunta real. El segundo es reintentar sin límite. Un correo duplicado molesta al usuario y hace dificil entender la tasa verdadera.&lt;/p&gt;

&lt;p&gt;Otro fallo común es poner la lógica de métricas dentro del proveedor de email. Si mañana cambias de proveedor, perderás continuidad. El evento debe pertenecer a tu producto y llevar una version del esquema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist final
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Cada solicitud tiene un &lt;code&gt;request_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;sent&lt;/code&gt;, &lt;code&gt;delivered&lt;/code&gt; y &lt;code&gt;next_step&lt;/code&gt; son eventos separados.&lt;/li&gt;
&lt;li&gt;[ ] Los reintentos son idempotentes.&lt;/li&gt;
&lt;li&gt;[ ] Los datos de prueba quedan fuera del panel de producción.&lt;/li&gt;
&lt;li&gt;[ ] El equipo puede responder qué acción cambia con cada métrica.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La lección es sencilla: el marketing de un SaaS también vive en los detalles del backend. Empieza con una señal, revisa el flujo cada semana y mejora solo aquello que ayuda a una persona a llegar a su primer momento de valor.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>marketing</category>
      <category>backend</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Un onboarding SaaS que mide cada señal</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Tue, 15 Sep 2026 02:22:58 +0000</pubDate>
      <link>https://dev.to/hannahdev56/un-onboarding-saas-que-mide-cada-senal-8of</link>
      <guid>https://dev.to/hannahdev56/un-onboarding-saas-que-mide-cada-senal-8of</guid>
      <description>&lt;p&gt;Cuando construí una primera versión de un SaaS, miraba una métrica casi todos los días: cuántos emails de bienvenida se enviaban. Parecía una señal razonable, pero no respondía la pregunta importante: ¿cuántas personas llegaron a usar una función útil?&lt;/p&gt;

&lt;p&gt;El envío de un mensaje no significa que el onboarding funcionó. Puede haber un error de entrega, un enlace roto, una pantalla confusa o simplemente demasiados pasos. Medir cada señal pequeña ayuda a encontrar el punto donde la experiencia pierde fuerza, y también evita cambiar el producto a ciegas.&lt;/p&gt;

&lt;h2&gt;
  
  
  El error de medir solo el correo enviado
&lt;/h2&gt;

&lt;p&gt;En un flujo típico hay varios momentos distintos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El usuario crea una cuenta.&lt;/li&gt;
&lt;li&gt;El backend acepta y guarda el registro.&lt;/li&gt;
&lt;li&gt;Se solicita la verificación del email.&lt;/li&gt;
&lt;li&gt;La persona abre el enlace.&lt;/li&gt;
&lt;li&gt;Completa una acción que demuestra valor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si solo guardamos &lt;code&gt;email_sent&lt;/code&gt;, todos esos momentos se mezclan. Un dashboard puede mostrar una bonita tasa de entrega mientras la activación real sigue siendo baja. Incluso un correo desechable usado durante una prueba interna puede crear una cuenta válida sin representar un usuario que volverá mañana.&lt;/p&gt;

&lt;p&gt;Por eso conviene separar los eventos. En mi caso, nombres simples como &lt;code&gt;signup_created&lt;/code&gt;, &lt;code&gt;verification_completed&lt;/code&gt; y &lt;code&gt;first_project_created&lt;/code&gt; fueron más útiles que un evento genérico llamado &lt;code&gt;onboarding_done&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato pequeño para cada señal
&lt;/h2&gt;

&lt;p&gt;Cada evento debería tener un contrato consistente. No hace falta comenzar con una plataforma enorme; una tabla o un log estructurado ya permite aprender.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"first_project_created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usr_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"occurred_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-15T09:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"onboarding_checklist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"template"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"simple"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guarda el nombre, el usuario, la hora y el origen. Las propiedades deben explicar una decisión real, no convertirse en un cajón de todo. Si un dato no ayuda a comparar cohortes o diagnosticar un fallo, probablemente sobra.&lt;/p&gt;

&lt;p&gt;También es importante que el backend acepte reintentos sin duplicar la señal. Un &lt;code&gt;event_id&lt;/code&gt; o una clave de idempotencia sencilla evita que un doble clic parezca dos proyectos creados. Ese detalle me ahorro bastante confusión al leer los primeros informes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo instrumentarlo en el backend
&lt;/h2&gt;

&lt;p&gt;Empieza con tres capas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Entrada:&lt;/strong&gt; valida la petición y registra el evento que realmente ocurrió, no el que esperabas que ocurriera.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistencia:&lt;/strong&gt; guarda los eventos junto con una hora del servidor y una clave única.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consulta:&lt;/strong&gt; prepara conteos por cohorte, por ejemplo usuarios registrados en la misma semana.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El email merece su propia separación. &lt;code&gt;verification_requested&lt;/code&gt; no es igual a &lt;code&gt;verification_completed&lt;/code&gt;. Si estás probando entregas, puedes usar una bandeja aislada o una dirección temporal, pero no mezcles esos datos con los resultados de clientes reales. En notas de QA a veces aparece “fake e mail com” cuando alguien busca una cuenta de prueba; es mejor que el flujo deje claro cuál es el entorno y qué población excluye.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un ejemplo de recorrido
&lt;/h2&gt;

&lt;p&gt;Imagina 100 registros durante una semana. 92 reciben el enlace, 70 lo abren y 38 crean su primer proyecto. El mayor problema no está necesariamente en el correo: también puede estar en la pantalla posterior a la verificación.&lt;/p&gt;

&lt;p&gt;Con esos cortes puedes preguntar algo concreto: ¿qué cambió entre abrir el enlace y crear un proyecto? Prueba una sola mejora, como mostrar una plantilla inicial, y compara cohortes equivalentes. El resultado será menos espectacular que mirar visitas totales, pero mucho más accionable.&lt;/p&gt;

&lt;p&gt;Un artículo anterior sobre &lt;a href="https://dev.to/hannahdev56/saas-emails-de-trial-con-senales-utiles-48i5"&gt;señales útiles en los emails de trial&lt;/a&gt; me ayudó a pensar en el mensaje como parte de un recorrido, no como una tarea aislada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Usar el frontend como única fuente:&lt;/strong&gt; si la pestaña se cierra, puedes perder el evento.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cambiar nombres cada semana:&lt;/strong&gt; rompe las comparaciones históricas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contar reintentos como éxito:&lt;/strong&gt; una petición repetida debe ser idempotente.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poner datos personales en cada evento:&lt;/strong&gt; registra solo lo necesario.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medir demasiadas cosas al principio:&lt;/strong&gt; un dashboard lleno no siempre explica más.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El término tempail mail puede aparecer en búsquedas internas o comentarios, pero no lo conviertas en una etiqueta de usuario ni en una decisión de producto. El contexto de la prueba importa más que la palabra usada para encontrarla.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist final
&lt;/h2&gt;

&lt;p&gt;Antes de publicar una nueva pantalla de onboarding, revisa:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Qué acción demuestra valor para el usuario?&lt;/li&gt;
&lt;li&gt;¿Existe un evento para cada paso importante?&lt;/li&gt;
&lt;li&gt;¿Los reintentos son seguros?&lt;/li&gt;
&lt;li&gt;¿Puedes comparar cohortes sin exportar datos manualmente?&lt;/li&gt;
&lt;li&gt;¿Las pruebas internas están separadas de producción?&lt;/li&gt;
&lt;li&gt;¿Cada métrica tiene una decisión asociada?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El aprendizaje principal es sencillo: el onboarding no es un email, sino una cadena de señales. Si empiezas con tres eventos bien definidos, puedes mejorar el producto con evidencia sin construir un sistema de analítica gigantesco desde el primer día.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>productivity</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
