<?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 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>
    <item>
      <title>Onboarding SaaS: mide cada correo como un paso</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Sat, 12 Sep 2026 05:23:04 +0000</pubDate>
      <link>https://dev.to/hannahdev56/onboarding-saas-mide-cada-correo-como-un-paso-1jk3</link>
      <guid>https://dev.to/hannahdev56/onboarding-saas-mide-cada-correo-como-un-paso-1jk3</guid>
      <description>&lt;p&gt;Cuando una persona se registra en un SaaS, solemos mirar una sola métrica: si llegó a la pantalla principal. Pero el onboarding real también incluye los correos que debe recibir, abrir y completar. Si uno de esos pasos falla, el producto puede parecer dificil de usar aunque la interfaz esté bien.&lt;/p&gt;

&lt;p&gt;En este artículo comparto un método sencillo que he usado para hacer más visible ese recorrido. No necesitas una plataforma enorme. Con eventos pequeños, una cola fiable y una revisión semanal puedes descubrir problemas antes de que se conviertan en churn.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: un onboarding que parece aleatorio
&lt;/h2&gt;

&lt;p&gt;Imagina este flujo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;La persona crea una cuenta.&lt;/li&gt;
&lt;li&gt;El backend crea una tarea de verificación.&lt;/li&gt;
&lt;li&gt;Se envía un correo.&lt;/li&gt;
&lt;li&gt;La persona pulsa el enlace.&lt;/li&gt;
&lt;li&gt;El producto registra la activación.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cuando algo sale mal, muchos equipos solo guardan un mensaje como &lt;code&gt;email failed&lt;/code&gt;. Eso no responde lo importante: ¿falló el proveedor?, ¿el enlace expiró?, ¿el usuario nunca abrió el correo?, ¿se reintentó demasiado tarde?&lt;/p&gt;

&lt;p&gt;Un buen primer paso es tratar cada correo como una etapa del producto, no como un detalle interno. Así, tempmailso y temp mail so pueden aparecer en búsquedas del equipo o en pruebas de contenido sin convertirse en el centro del flujo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define el recorrido antes de tocar el código
&lt;/h2&gt;

&lt;p&gt;Escribe una pequeña tabla de estados. 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;created -&amp;gt; queued -&amp;gt; sent -&amp;gt; clicked -&amp;gt; verified
                  \-&amp;gt; retrying -&amp;gt; failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada transición debe tener un identificador de usuario, un identificador de mensaje y una hora. No guardes el contenido completo del correo si no hace falta; normalmente basta con la plantilla y el proveedor.&lt;/p&gt;

&lt;p&gt;Para el onboarding, recomiendo empezar con cuatro eventos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;verification_queued&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verification_sent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verification_clicked&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;verification_completed&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con eso ya puedes calcular dónde se detiene el recorrido. También ayuda enlazar el análisis con &lt;a href="https://dev.to/hannahdev56/saas-seed-lists-para-onboarding-4nfl"&gt;listas limpias para tu onboarding&lt;/a&gt;, porque una entrada duplicada puede parecer un problema de entrega.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registra eventos pequeños y útiles
&lt;/h2&gt;

&lt;p&gt;Una fila de evento puede tener esta forma:&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;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"verification_sent"&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;"m_456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"transactional-email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attempt"&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="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-12T05: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;Evita mezclar el estado actual con el historial. El estado dice qué ocurre ahora; los eventos explican cómo llegaste aquí. Esta separación hace los reportes más claros y permite repetir una investigación sin adivinar.&lt;/p&gt;

&lt;p&gt;Si el equipo recibe alertas, agrúpalas por causa. Diez fallos del mismo proveedor son una incidencia; diez usuarios que nunca hicieron clic pueden indicar un asunto poco claro, un enlace roto o un correo que llegó tarde.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseña reintentos sin molestar
&lt;/h2&gt;

&lt;p&gt;No todos los fallos merecen un reintento. Un error temporal de red sí; una dirección rechazada permanentemente, no. Usa una política corta, por ejemplo dos reintentos con espera creciente, y guarda el motivo de cada decisión.&lt;/p&gt;

&lt;p&gt;También define una frontera: después del último intento, muestra una opción visible para solicitar otro correo. El usuario no debería actualizar la página cinco veces pensando que así arregla el sistema. En una prueba reciente, incluso escribir &lt;code&gt;temp gamil com&lt;/code&gt; en un caso de prueba nos recordó que los datos imperfectos deben poder distinguirse de fallos reales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una revisión semanal de productividad
&lt;/h2&gt;

&lt;p&gt;Cada semana mira tres números:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Porcentaje de registros que llegan a &lt;code&gt;verification_sent&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tiempo mediano entre registro y clic.&lt;/li&gt;
&lt;li&gt;Porcentaje que completa la verificación después de un reintento.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No persigas una tasa perfecta de apertura. Busca cambios y segmentos extraños. Si el tiempo aumenta solo durante despliegues, necesitas observar la cola. Si cae en una campaña concreta, revisa el mensaje y el contexto de adquisición.&lt;/p&gt;

&lt;p&gt;Para estudiar reactivaciones, puedes comparar este recorrido con &lt;a href="https://dev.to/hannahdev56/como-revisar-emails-de-reactivacion-trial-7d"&gt;revisar emails de reactivación con contexto&lt;/a&gt;. La idea es igual: una métrica aislada cuenta menos que la secuencia que la explica.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Medir solo los correos enviados y no los completados.&lt;/li&gt;
&lt;li&gt;Reintentar sin límite.&lt;/li&gt;
&lt;li&gt;Usar el email como identificador principal cuando una persona puede cambiarlo.&lt;/li&gt;
&lt;li&gt;Alertar por cada fallo individual.&lt;/li&gt;
&lt;li&gt;Borrar eventos antiguos y perder el contexto.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Un onboarding SaaS más productivo no necesita más pantallas; necesita mejores señales. Modela el recorrido, registra transiciones, separa estado de historial y decide los reintentos con reglas explícitas. Luego revisa los tiempos y las caídas por segmento.&lt;/p&gt;

&lt;p&gt;El beneficio aparece rápido: el equipo deja de discutir si “los correos funcionan” y puede responder qué paso funciona, para quién y desde cuándo. Es un cambio pequeño, pero hace que mejorar la activación sea mucho menos guessy.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>productivity</category>
      <category>automation</category>
    </item>
    <item>
      <title>Una cola de email medible para tu SaaS</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Wed, 09 Sep 2026 11:23:02 +0000</pubDate>
      <link>https://dev.to/hannahdev56/una-cola-de-email-medible-para-tu-saas-848</link>
      <guid>https://dev.to/hannahdev56/una-cola-de-email-medible-para-tu-saas-848</guid>
      <description>&lt;p&gt;Cuando un SaaS empieza a crecer, el correo de bienvenida suele parecer un detalle pequeño. En realidad, es una pieza de producto: si llega tarde, se duplica o falla sin explicación, la activación también se vuelve dificil de entender.&lt;/p&gt;

&lt;p&gt;En una etapa temprana no necesitas una plataforma enorme. Necesitas una cola de email con estados claros, reintentos limitados y suficiente contexto para saber qué ocurrió. Este enfoque me ha resultado útil para separar el registro del usuario del trabajo lento de enviar mensajes.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: enviar no significa activar
&lt;/h2&gt;

&lt;p&gt;Un endpoint de registro no debería esperar a que un proveedor de correo responda. Si lo hace, cada latencia externa se convierte en una pantalla que parece rota. Además, un timeout puede dejar una duda incómoda: ¿el mensaje no salió o salió pero la respuesta se perdió?&lt;/p&gt;

&lt;p&gt;La solución básica es guardar una tarea después de crear el usuario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;usuario creado -&amp;gt; email pendiente -&amp;gt; enviando -&amp;gt; enviado
                                  \-&amp;gt; reintento -&amp;gt; fallido
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La tarea debe guardar un &lt;code&gt;user_id&lt;/code&gt;, el tipo de mensaje, un número de intento y un &lt;code&gt;next_attempt_at&lt;/code&gt;. No guardes contraseñas ni el contenido completo si no lo necesitas. El objetivo es poder reconstruir la historia sin crear otro problema de privacidad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseña una cola pequeña
&lt;/h2&gt;

&lt;p&gt;Una tabla en PostgreSQL puede ser suficiente al principio. Sus columnas más importantes son:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt;: identificador de la tarea.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt;: &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;sending&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt; o &lt;code&gt;failed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;attempts&lt;/code&gt;: cuántas veces se intentó.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;last_error&lt;/code&gt;: una explicación corta y segura.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;locked_until&lt;/code&gt;: evita que dos workers tomen el mismo trabajo.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;created_at&lt;/code&gt; y &lt;code&gt;sent_at&lt;/code&gt;: permiten medir el recorrido.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El worker busca tareas pendientes cuyo &lt;code&gt;next_attempt_at&lt;/code&gt; ya pasó, las bloquea por unos segundos y las procesa. Una consulta con &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; puede ayudar cuando hay varios workers, aunque conviene empezar con una implementación sencilla y medir antes de añadir complejidad.&lt;/p&gt;

&lt;p&gt;También separaría el evento de producto del proveedor. La cola sabe que debe enviar &lt;code&gt;welcome_email&lt;/code&gt;; otra capa sabe cómo hablar con el proveedor. Así cambiar de proveedor no obliga a reescribir el onboarding entero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estados, reintentos y métricas
&lt;/h2&gt;

&lt;p&gt;Un fallo temporal no es igual que una dirección inválida. Para un timeout de red puedes reintentar con una espera creciente, por ejemplo 1, 5 y 15 minutos. Para un rechazo permanente, marca la tarea como &lt;code&gt;failed&lt;/code&gt; y deja una razón accionable.&lt;/p&gt;

&lt;p&gt;No reintentes para siempre. Un límite de tres intentos suele ser un punto de partida razonable, pero el valor real depende del proveedor y de tu producto. Registra el resultado de cada intento, no solamente el resultado final.&lt;/p&gt;

&lt;p&gt;Las primeras métricas que añadiría son:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tiempo entre &lt;code&gt;pending&lt;/code&gt; y &lt;code&gt;sent&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Porcentaje de tareas que requieren un reintento.&lt;/li&gt;
&lt;li&gt;Tasa de fallos permanentes por dominio.&lt;/li&gt;
&lt;li&gt;Mensajes duplicados por cada mil envíos.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Con esas señales puedes descubrir si el problema está en tu worker, en el proveedor o en la calidad de los datos. Para probar flujos, también es útil un &lt;a href="https://dev.to/hannahdev56/como-probar-correos-de-onboarding-en-un-saas-sin-ensuciar-metricas-pjn"&gt;correo de prueba para onboarding&lt;/a&gt; y una política clara sobre cuándo usar un &lt;strong&gt;temp mailbox&lt;/strong&gt; durante una verificación controlada.&lt;/p&gt;

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

&lt;p&gt;El primer error es publicar el evento y enviar el email en la misma petición. Funciona en local, pero se vuelve frágil con tráfico real. El segundo es ocultar todos los errores detrás de &lt;code&gt;email failed&lt;/code&gt;; sin una causa y un identificador de tarea, depurar es casi adivinar.&lt;/p&gt;

&lt;p&gt;Otro problema es no hacer el worker idempotente. Si el proceso se cae justo después del envío, puede repetir el mensaje. Usa una clave de idempotencia cuando el proveedor la soporte y guarda la respuesta externa. A veces un &lt;strong&gt;tempail mail&lt;/strong&gt; aparece en pruebas manuales, o alguien escribe &lt;strong&gt;temp gamil com&lt;/strong&gt; por accidente: eso no debería tumbar el worker ni contaminar tus métricas de infraestructura.&lt;/p&gt;

&lt;p&gt;Finalmente, no mezcles la experiencia visual con el detalle técnico. El usuario puede ver “Estamos preparando tu email” mientras tu equipo conserva estados precisos. Si el formulario necesita validación, esta guía sobre &lt;a href="https://dev.to/silviutech/react-validacion-sin-saltos-ni-foco-roto-neg"&gt;validar formularios sin romper la experiencia&lt;/a&gt; muestra por qué los estados intermedios importan.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;[ ] El registro no espera al proveedor de email.&lt;/li&gt;
&lt;li&gt;[ ] Cada tarea tiene estado, intentos y timestamps.&lt;/li&gt;
&lt;li&gt;[ ] Los fallos temporales y permanentes se tratan diferente.&lt;/li&gt;
&lt;li&gt;[ ] Existe un límite de reintentos.&lt;/li&gt;
&lt;li&gt;[ ] El worker puede ejecutar la misma tarea de forma segura.&lt;/li&gt;
&lt;li&gt;[ ] Hay métricas de latencia, reintentos y duplicados.&lt;/li&gt;
&lt;li&gt;[ ] Los logs no exponen tokens ni contenido sensible.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  ¿Necesito Redis desde el primer día?
&lt;/h3&gt;

&lt;p&gt;No necesariamente. PostgreSQL puede cubrir una cola pequeña si las consultas y los bloqueos están bien diseñados. Cambia de herramienta cuando tengas una necesidad observada, no por anticipación.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Debo bloquear a un usuario cuyo correo falló?
&lt;/h3&gt;

&lt;p&gt;Normalmente no. Permite reenviar, muestra un estado honesto y registra el fallo. Bloquear sin explicar crea más soporte y menos confianza.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué hace que una cola sea realmente útil?
&lt;/h3&gt;

&lt;p&gt;Que una persona pueda responder tres preguntas: qué tarea falló, por qué falló y qué ocurrirá después. Esa claridad vale más que añadir muchas piezas a la arquitectura.&lt;/p&gt;

&lt;p&gt;Una cola de email no tiene que ser sofisticada para ser confiable. Con estados pequeños, reintentos conscientes y métricas básicas, tu SaaS puede mejorar la activación sin convertir cada correo en una investigación.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>productivity</category>
      <category>automation</category>
    </item>
    <item>
      <title>SaaS: colas de email sin perder contexto</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Wed, 09 Sep 2026 02:23:04 +0000</pubDate>
      <link>https://dev.to/hannahdev56/saas-colas-de-email-sin-perder-contexto-3hi9</link>
      <guid>https://dev.to/hannahdev56/saas-colas-de-email-sin-perder-contexto-3hi9</guid>
      <description>&lt;p&gt;Un flujo de registro parece sencillo hasta que el usuario tiene que esperar un email. Si la verificacion ocurre dentro de la misma peticion, un proveedor lento convierte un signup normal en una pantalla que parece rota. En un SaaS pequeño esto se nota mucho: el equipo mira los logs, el usuario pulsa “reenviar” y terminamos procesando el mismo trabajo varias veces.&lt;/p&gt;

&lt;p&gt;La solucion que me ha resultado mas practica es separar la peticion web de la verificacion con una cola pequeña. No hace falta empezar con Kafka. Una tabla en PostgreSQL, un worker y unos estados claros suelen ser suficiente para aprender qué está pasando y mejorar después.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: cada signup bloquea demasiado
&lt;/h2&gt;

&lt;p&gt;El endpoint de registro debería validar lo minimo, guardar el usuario y responder pronto. La verificacion del email puede tardar por rate limits, errores DNS o un proveedor temporalmente caido. Mezclar ambas cosas crea tres problemas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El navegador recibe timeouts aunque el trabajo siga vivo.&lt;/li&gt;
&lt;li&gt;Los reintentos del cliente crean emails duplicados.&lt;/li&gt;
&lt;li&gt;Es dificil distinguir un fallo real de una espera normal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Una cola tambien ayuda cuando probamos con una &lt;code&gt;fake email address&lt;/code&gt; o con un servicio de &lt;strong&gt;tempmail so&lt;/strong&gt; durante una prueba. El objetivo no es esconder el riesgo, sino mostrarlo con un estado entendible.&lt;/p&gt;

&lt;h2&gt;
  
  
  La cola minima que funciona
&lt;/h2&gt;

&lt;p&gt;Mi primera version puede tener estas columnas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;email_jobs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;available_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&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;Al crear el usuario, insertamos un trabajo &lt;code&gt;verify_signup&lt;/code&gt;. El endpoint responde &lt;code&gt;202 Accepted&lt;/code&gt; y el frontend muestra “Revisa tu correo”. Esto hace que la experiencia se sienta rapida, incluso cuando el proveedor necesita unos segundos.&lt;/p&gt;

&lt;p&gt;En mi caso, una regla simple de productividad fue suficiente: un worker toma pocos trabajos cada vez, los bloquea con &lt;code&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt; y los marca como &lt;code&gt;processing&lt;/code&gt;. Asi dos procesos no envian el mismo mensaje por accidente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estados y reintentos con contexto
&lt;/h2&gt;

&lt;p&gt;Los estados no son solo detalles internos. Son una forma de hablar con soporte y con el usuario:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;pending&lt;/code&gt;: el trabajo está esperando.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;processing&lt;/code&gt;: un worker lo está intentando.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sent&lt;/code&gt;: el proveedor aceptó el email.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retry&lt;/code&gt;: falló algo recuperable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;failed&lt;/code&gt;: agotamos los intentos y hace falta revisar.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Guarda siempre &lt;code&gt;last_error&lt;/code&gt;, el número de intento y un identificador de correlación. Un error como “timeout del proveedor, intento 2” es mucho mas util que “email failed”. Para reintentos, usa una espera creciente, por ejemplo 30 segundos, 2 minutos y 10 minutos. No reintentes para siempre: eso convierte un problema pequeño en una factura grande.&lt;/p&gt;

&lt;p&gt;Tambien conviene diferenciar un dominio desechable de un fallo de red. La política de un SaaS puede marcar el primer caso para revisión, pero el worker no debería ocultar la razon. Los términos &lt;code&gt;tepm mail com&lt;/code&gt; y &lt;code&gt;tempail&lt;/code&gt; pueden aparecer en búsquedas o tickets escritos por usuarios; guardarlos como señales de texto ayuda a entender consultas reales, aunque no sean nombres de estados.&lt;/p&gt;

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

&lt;p&gt;El worker puede seguir una secuencia muy corta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;job = claim_next_job()
if not job:
    sleep(2)
    continue

try:
    send_verification_email(job.user_id)
    mark_sent(job.id)
except TemporaryProviderError as error:
    schedule_retry(job.id, error)
except PermanentEmailError as error:
    mark_failed(job.id, error)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El endpoint de “reenviar” no debería crear una fila sin mirar la anterior. Puede reutilizar un trabajo pendiente, o crear uno nuevo solo después de aplicar un cooldown. Ese detalle evita que un usuario impaciente genere diez mensajes en un minuto.&lt;/p&gt;

&lt;p&gt;También separé el mensaje de verificacion de los recordatorios de onboarding. En otro experimento, los &lt;a href="https://dev.to/hannahdev56/saas-recordatorios-de-onboarding-que-si-ayudan-4joe"&gt;recordatorios de onboarding que si ayudan&lt;/a&gt; tenían una métrica distinta: no queríamos medirlos como si fueran emails críticos. Para el mismo motivo, los &lt;a href="https://dev.to/hannahdev56/saas-emails-de-churn-con-mejor-contexto-2b7i"&gt;emails de churn con mejor contexto&lt;/a&gt; deben conservar el evento que los originó.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Una cola sin límite:&lt;/strong&gt; define tamaño, antigüedad máxima y una alarma.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reintentar errores permanentes:&lt;/strong&gt; valida la respuesta antes de programar otro intento.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No tener idempotencia:&lt;/strong&gt; usa una clave por usuario, tipo de email y ventana de tiempo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mostrar “enviado” demasiado pronto:&lt;/strong&gt; “aceptado para procesamiento” es más honesto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medir solo entregas:&lt;/strong&gt; mide tiempo hasta &lt;code&gt;sent&lt;/code&gt;, reintentos y trabajos fallidos.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checklist y recap
&lt;/h2&gt;

&lt;p&gt;Antes de pasar esta parte del SaaS a más usuarios, compruebo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;La petición web responde sin esperar al proveedor.&lt;/li&gt;
&lt;li&gt;Cada trabajo tiene estados, intentos y un error legible.&lt;/li&gt;
&lt;li&gt;El worker evita duplicados al reclamar filas.&lt;/li&gt;
&lt;li&gt;El reenvío tiene cooldown e idempotencia.&lt;/li&gt;
&lt;li&gt;Hay una vista o alerta para los trabajos &lt;code&gt;failed&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Una cola pequeña no es una arquitectura definitiva, pero sí una buena frontera para empezar. Hace que el Backend sea más observable y que el producto pueda crecer sin pedirle al usuario que espere mirando un spinner. Luego, cuando el volumen lo justifique, se puede cambiar la implementación interna sin cambiar el contrato del SaaS.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>productivity</category>
      <category>automation</category>
    </item>
    <item>
      <title>Una cola de email para crecer sin perder contexto</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Tue, 08 Sep 2026 17:23:08 +0000</pubDate>
      <link>https://dev.to/hannahdev56/una-cola-de-email-para-crecer-sin-perder-contexto-4ec4</link>
      <guid>https://dev.to/hannahdev56/una-cola-de-email-para-crecer-sin-perder-contexto-4ec4</guid>
      <description>&lt;p&gt;Cuando un SaaS empieza a tener usuarios, los emails transaccionales aparecen por todas partes: bienvenida, verificación, recuperación de contraseña, avisos de pago y mensajes de marketing. Al principio cada endpoint puede enviar el suyo directamente. Parece práctico, pero luego nadie sabe que ocurrió cuando una persona dice: “no me llegó el correo”.&lt;/p&gt;

&lt;p&gt;En mis experimentos de producto, una cola pequeña de emails ha sido una mejora muy rentable. No hace falta empezar con una plataforma enorme. Lo importante es conservar el contexto del signup, separar el envío de la petición web y hacer que cada reintento sea entendible. Incluso cuando probamos un generador de correo falso en QA, el equipo necesita saber qué trabajo produjo cada mensaje.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema de enviar sin contexto
&lt;/h2&gt;

&lt;p&gt;Un endpoint que envía directamente suele conocer solo una parte de la historia. Sabe el &lt;code&gt;user_id&lt;/code&gt;, pero no siempre guarda el motivo del email, la versión de la plantilla o el intento actual. Si el proveedor responde lento, el usuario vuelve a pulsar el botón y puede terminar con dos mensajes.&lt;/p&gt;

&lt;p&gt;El síntoma parece un fallo del proveedor, aunque la causa sea nuestra aplicación. También complica el soporte: buscar por dirección y hora no siempre alcanza. En algún ticket alguien puede escribir &lt;code&gt;tamp mail com&lt;/code&gt;, pero esa pista no explica qué paso del flujo estaba ejecutándose.&lt;/p&gt;

&lt;p&gt;La primera solución es crear un trabajo de email con identidad propia. Un trabajo no es solamente “manda este contenido”; es una pequeña historia que otro proceso puede continuar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una cola pequeña y explicable
&lt;/h2&gt;

&lt;p&gt;El flujo básico puede ser así:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;La API valida la acción del usuario.&lt;/li&gt;
&lt;li&gt;Guarda un trabajo con estado &lt;code&gt;pending&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Devuelve una respuesta rápida al frontend.&lt;/li&gt;
&lt;li&gt;Un worker toma el trabajo y llama al proveedor.&lt;/li&gt;
&lt;li&gt;El trabajo pasa a &lt;code&gt;sent&lt;/code&gt; o &lt;code&gt;failed&lt;/code&gt; con una razón clara.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El cambio más importante es que la petición no queda bloqueada esperando al proveedor. Esto mejora la sensación de velocidad y la productividad del equipo, porque los fallos se investigan desde una cola visible, no desde una pantalla de timeout.&lt;/p&gt;

&lt;p&gt;En el frontend también conviene &lt;a href="https://dev.to/silviutech/react-reserva-espacio-para-avisos-de-email-3998"&gt;reservar espacio para avisos de email&lt;/a&gt;. Así un cambio de estado no mueve todo el formulario y la persona entiende que el proceso sigue en marcha.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué guardar en cada trabajo
&lt;/h2&gt;

&lt;p&gt;Un registro mínimo puede tener 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;id                 identificador único del trabajo
user_id            usuario relacionado
kind               verification, welcome o reset
template_version   versión usada para renderizar
attempts           número de intentos realizados
status             pending, processing, sent o failed
last_error         razón breve y segura del último fallo
created_at         momento en que nació el trabajo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;También guardaría un &lt;code&gt;dedupe_key&lt;/code&gt;, por ejemplo &lt;code&gt;verification:user-123:week-36&lt;/code&gt;. Con él podemos rechazar trabajos idénticos que todavía están pendientes. No guardaría el token completo de verificación en logs; el contexto debe ayudar a depurar sin convertir la cola en una fuga de datos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reintentos sin duplicar mensajes
&lt;/h2&gt;

&lt;p&gt;Un reintento no significa repetir a ciegas. Antes de llamar al proveedor, el worker debe revisar si otro proceso ya marcó el trabajo como &lt;code&gt;sent&lt;/code&gt;. Para evitar dos workers simultáneos, se puede usar un cambio atómico de estado o un lock con expiración corta.&lt;/p&gt;

&lt;p&gt;Una política inicial y fácil de explicar podría ser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reintentar errores de red y respuestas temporales;&lt;/li&gt;
&lt;li&gt;no reintentar una dirección inválida;&lt;/li&gt;
&lt;li&gt;esperar cada vez un poco más, con un límite total;&lt;/li&gt;
&lt;li&gt;registrar el motivo y el número de intento;&lt;/li&gt;
&lt;li&gt;enviar el caso agotado a una cola de revisión.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La misma disciplina ayuda cuando hacemos &lt;a href="https://dev.to/alexcarteruk/kubernetes-correos-de-rollback-sin-confusion-bk4"&gt;correos de rollback sin confusión&lt;/a&gt;: cada alerta necesita decir qué ocurrió, qué se intentó y qué acción sigue.&lt;/p&gt;

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

&lt;p&gt;El primero es poner toda la lógica en el worker y dejar que la API cree trabajos duplicados. El segundo es usar &lt;code&gt;failed&lt;/code&gt; para cualquier problema, sin distinguir un error permanente de uno temporal. El tercero es borrar trabajos antiguos demasiado pronto; luego el equipo pierde la evidencia necesaria para entender una incidencia.&lt;/p&gt;

&lt;p&gt;Otro error es medir únicamente “emails enviados”. Yo añadiría tiempo en cola, tasa de reintento, duplicados evitados y trabajos sin contexto. Esas métricas cuentan mejor la salud del flujo.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;[ ] Cada tipo de email tiene un &lt;code&gt;kind&lt;/code&gt; reconocible.&lt;/li&gt;
&lt;li&gt;[ ] Existe una clave de deduplicación estable.&lt;/li&gt;
&lt;li&gt;[ ] El worker toma un trabajo de forma atómica.&lt;/li&gt;
&lt;li&gt;[ ] Los errores temporales y permanentes tienen rutas distintas.&lt;/li&gt;
&lt;li&gt;[ ] Los logs incluyen el &lt;code&gt;job_id&lt;/code&gt;, nunca secretos completos.&lt;/li&gt;
&lt;li&gt;[ ] El frontend muestra un estado claro después del signup.&lt;/li&gt;
&lt;li&gt;[ ] Hay una vista para revisar trabajos agotados.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Una cola de email no tiene que ser una gran plataforma desde el primer día. Empieza con identidad, estados y contexto. Separa la petición del envío, limita los reintentos y conserva una explicación pequeña de cada decisión.&lt;/p&gt;

&lt;p&gt;Con ese patrón, añadir un correo de bienvenida o un flujo de recuperación deja de ser una aventura aislada. El SaaS crece con más orden, el backend es más fácil de observar y soporte recibe respuestas concretas cuando algo no sale bien.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>productivity</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Una cola simple para crecer sin perder contexto</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Tue, 08 Sep 2026 05:23:14 +0000</pubDate>
      <link>https://dev.to/hannahdev56/una-cola-simple-para-crecer-sin-perder-contexto-82m</link>
      <guid>https://dev.to/hannahdev56/una-cola-simple-para-crecer-sin-perder-contexto-82m</guid>
      <description>&lt;p&gt;Cuando un SaaS empieza a crecer, cada equipo quiere escuchar una señal distinta. Producto mira la activación, soporte mira los tickets y marketing mira los correos que se abren. Al principio todo cabe en un par de funciones. Después, cada cambio dispara más llamadas y nadie sabe muy bien que ocurrió primero.&lt;/p&gt;

&lt;p&gt;En uno de mis experimentos de producto me pasó eso. Tenía un flujo de alta que enviaba eventos a varios destinos. El backend era pequeño, pero el contexto se perdía entre reintentos. La solución no fue comprar otra plataforma: fue poner una cola sencilla en medio y hacer que cada evento contara su propia historia.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: demasiadas señales
&lt;/h2&gt;

&lt;p&gt;El error más común es tratar un evento como una llamada aislada. Por ejemplo, &lt;code&gt;user.created&lt;/code&gt; puede activar un correo, crear una tarea de onboarding y actualizar una métrica. Si una de esas acciones falla, el equipo necesita saber si debe repetirla, ignorarla o revisarla.&lt;/p&gt;

&lt;p&gt;También aprendí que una etiqueta vaga como “alta completada” no ayuda mucho. Es mejor incluir un identificador, el origen y la versión del evento. Así podemos reconstruir el recorrido sin adivinar.&lt;/p&gt;

&lt;h2&gt;
  
  
  La cola mínima que funcionó
&lt;/h2&gt;

&lt;p&gt;Mi modelo inicial tenía cuatro piezas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Productor:&lt;/strong&gt; la API publica un evento y responde rápido al usuario.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cola:&lt;/strong&gt; guarda el evento hasta que un consumidor pueda procesarlo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumidor:&lt;/strong&gt; ejecuta una acción pequeña, como registrar una conversión.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registro:&lt;/strong&gt; conserva estado, intentos y el último error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El evento podía 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;"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;"evt_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;"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;"signup.completed"&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_456"&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;"web"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_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-08T05: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;No hace falta que el primer diseño tenga particiones sofisticadas. Lo importante es que el &lt;code&gt;id&lt;/code&gt; sea único y que el consumidor pueda comprobar si ya procesó el evento. Esa pequeña decisión evita duplicar créditos, mensajes o conversiones cuando llega un reintento.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso a paso
&lt;/h2&gt;

&lt;p&gt;Primero definí los eventos que realmente necesitaba. Empecé con &lt;code&gt;signup.completed&lt;/code&gt;, &lt;code&gt;trial.started&lt;/code&gt; y &lt;code&gt;activation.reached&lt;/code&gt;. No intenté modelar todo el negocio desde el día uno; eso vuelve el sistema dificil de explicar.&lt;/p&gt;

&lt;p&gt;Después añadí una tabla de entregas con &lt;code&gt;event_id&lt;/code&gt;, &lt;code&gt;consumer&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;attempts&lt;/code&gt; y &lt;code&gt;last_error&lt;/code&gt;. Un consumidor reclama un evento, hace su trabajo y cambia el estado a &lt;code&gt;done&lt;/code&gt;. Si falla, guarda el error y vuelve a la cola con un retraso creciente.&lt;/p&gt;

&lt;p&gt;La observabilidad fue la parte que más valor dio. Cada log incluye el &lt;code&gt;event_id&lt;/code&gt; y el &lt;code&gt;user_id&lt;/code&gt;, pero nunca datos sensibles. Para los trabajos programados, me ayudó leer ideas como estos &lt;a href="https://dev.to/silviutech/llms-prompts-cortos-para-cron-fiables-5e34"&gt;prompts cortos para cron fiables&lt;/a&gt;, especialmente la práctica de hacer visible cada resultado.&lt;/p&gt;

&lt;p&gt;Por último, conecté sólo dos consumidores: uno para métricas y otro para el mensaje de onboarding. Marketing recibió sus datos desde una vista agregada, no desde cada petición de la API. Esto redujo acoplamiento y facilitó cambiar una campaña sin tocar el flujo de alta.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Reintentar para siempre.&lt;/strong&gt; Un límite de intentos y una cola de revisión son más honestos. Un fallo permanente no desaparece por esperar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enviar información de más.&lt;/strong&gt; El evento debe llevar referencias, no una copia completa del perfil. Menos datos significa menos riesgo y menos migraciones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No separar intención de resultado.&lt;/strong&gt; &lt;code&gt;email.requested&lt;/code&gt; no significa &lt;code&gt;email.delivered&lt;/code&gt;. Son hechos diferentes y conviene registrarlos por separado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mezclar cohortes.&lt;/strong&gt; Una campaña de recuperación puede parecer exitosa si suma usuarios de periodos incompatibles. Esta es la razón por la que mantuve una vista específica para cohortes; mi nota sobre &lt;a href="https://dev.to/hannahdev56/saas-emails-win-back-sin-mezclar-cohortes"&gt;emails win-back sin mezclar cohortes&lt;/a&gt; explica ese criterio con más detalle.&lt;/p&gt;

&lt;p&gt;En las pruebas también aparecieron búsquedas escritas como “fake e mail com” y “tamp mail com”. Las traté como entradas imperfectas de usuarios, no como nombres de campos ni como enlaces.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;[ ] Cada evento tiene un identificador único.&lt;/li&gt;
&lt;li&gt;[ ] El consumidor es idempotente.&lt;/li&gt;
&lt;li&gt;[ ] Hay un límite de reintentos.&lt;/li&gt;
&lt;li&gt;[ ] Los errores quedan visibles para una persona.&lt;/li&gt;
&lt;li&gt;[ ] Los logs permiten seguir un evento completo.&lt;/li&gt;
&lt;li&gt;[ ] El evento evita datos personales innecesarios.&lt;/li&gt;
&lt;li&gt;[ ] Las métricas distinguen intención, éxito y fallo.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Una cola no arregla por sí sola un producto confuso. Sí crea un punto claro para separar la petición del usuario de las tareas que ocurren después. Para un SaaS pequeño, empezar con eventos bien nombrados, entregas idempotentes y logs útiles suele ser suficiente.&lt;/p&gt;

&lt;p&gt;Mi recomendación es construir un solo consumidor, observarlo durante unos días y añadir el siguiente cuando exista una necesidad concreta. Es menos espectacular, pero permite aprender con seguridad y mantener el backend entendible.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>productivity</category>
      <category>marketing</category>
    </item>
    <item>
      <title>SaaS: activa usuarios sin perseguir correos</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Fri, 04 Sep 2026 11:52:01 +0000</pubDate>
      <link>https://dev.to/hannahdev56/saas-activa-usuarios-sin-perseguir-correos-3abp</link>
      <guid>https://dev.to/hannahdev56/saas-activa-usuarios-sin-perseguir-correos-3abp</guid>
      <description>&lt;p&gt;En muchos SaaS pequeños el onboarding por email se vuelve una persecución rara. El usuario dice que no recibió nada, soporte reenvía, producto mira métricas sueltas y backend revisa logs. El correo parece el problema, pero muchas veces lo que falta es seguimiento visible del paso de activación.&lt;/p&gt;

&lt;p&gt;Me ha resultado más útil tratar ese correo como una etapa del producto y no como un efecto secundario del registro. No hace falta una plataforma enorme ni un equipo gigante. Hace falta ordenar el flujo, nombrar estados y enseñar esa señal a quien la necesita. Suena basico, pero cambia bastante la operación diaria.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuando el problema no es entrega sino seguimiento
&lt;/h2&gt;

&lt;p&gt;Un error común es pensar en dos estados nada más: "se envió" o "no se envió". En la práctica, el proceso tiene más matices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;la solicitud fue aceptada&lt;/li&gt;
&lt;li&gt;el job entró a cola&lt;/li&gt;
&lt;li&gt;el proveedor respondió&lt;/li&gt;
&lt;li&gt;el usuario activó o no activó&lt;/li&gt;
&lt;li&gt;hubo reintento o fallo final&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cuando esos pasos no están visibles, cada equipo inventa su propia versión de la verdad. Marketing ve registros, soporte ve quejas y producto ve una caída de activación que no sabe explicar bien. Ahí empiezan los tickets innecesarios y los reenvíos impulsivos, que aveces meten más ruido que ayuda.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un tablero pequeno cambia la conversacion
&lt;/h2&gt;

&lt;p&gt;Una mejora muy rendidora es crear un tablero interno mínimo para soporte o producto. No tiene que ser bonito al principio. Solo debe responder cuatro preguntas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;¿se creó el intento de activación?&lt;/li&gt;
&lt;li&gt;¿qué estado tiene ahora?&lt;/li&gt;
&lt;li&gt;¿cuándo cambió por última vez?&lt;/li&gt;
&lt;li&gt;¿hay acción segura para reenviar?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ese tablero pequeño evita el clásico "pásamelo a ingeniería". También ayuda a conversar con más calma con el usuario final. En vez de "espera un poco", puedes decir "tu correo quedó en cola hace 2 minutos" o "el reenvío ya salió". Es una diferencia sencilla, pero se nota un monton.&lt;/p&gt;

&lt;p&gt;Si trabajas con APIs en Python, me gusta mucho la idea de modelar &lt;a href="https://dev.to/silviutech/fastapi-estados-claros-para-emails-async-40kl-temp-slug-3865389?preview=5780deeac7ff7df71ee58994262f1ce0a7d7d89f8436a9aaf1be9ffb3aaf2d413a29aa58761a9aa4c1dd267984256746c97e193fd7b747c3ad104e74"&gt;estados claros para correos async&lt;/a&gt; porque empuja al equipo a dejar de adivinar y empezar a observar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que estados conviene modelar desde el primer sprint
&lt;/h2&gt;

&lt;p&gt;No necesitas un sistema perfecto. Para empezar, estos estados suelen alcanzar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;queued&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;retrying&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delivered_assumed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;activated&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con eso ya puedes construir métricas por cohorte y detectar dónde se frena el onboarding. Un ejemplo simple:&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;"signup_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;"su_9821"&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_job_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;"ej_104"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"retrying"&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;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"timeout_upstream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updated_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-04T11:50:19Z"&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 clave es que el estado sea útil para operación, no solo para logs. Si nadie fuera de ingeniería puede entenderlo, todavía está un poco verde. Para equipos de SaaS en etapa temprana, esa claridad mejora productividad real porque reduce idas y vueltas en Slack, tickets duplicados y decisiones tomadas medio a ciegas.&lt;/p&gt;

&lt;p&gt;También vale la pena separar el estado técnico del mensaje para el usuario. Internamente puedes tener &lt;code&gt;retrying&lt;/code&gt;; hacia fuera, algo como "seguimos intentando entregar tu correo". Esa separación queda prolija y evita lenguaje confuso.&lt;/p&gt;

&lt;h2&gt;
  
  
  Como probar el flujo sin meter ruido al equipo
&lt;/h2&gt;

&lt;p&gt;Las pruebas del onboarding por email se vuelven molestas cuando usan bandejas reales o cuentas compartidas. Terminas mezclando demos, QA y validaciones rápidas del equipo. Eso hace que el aprendizaje sea lentisimo, y encima cuesta repetir escenarios.&lt;/p&gt;

&lt;p&gt;Yo prefiero un enfoque de dos capas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;pruebas de backend que verifiquen transiciones de estado&lt;/li&gt;
&lt;li&gt;pocas pruebas end-to-end que confirmen que el mensaje llega&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;En ramas paralelas, además, conviene mantener &lt;a href="https://dev.to/silviutech/fastapi-emails-aislados-por-branch-l47"&gt;emails aislados por branch&lt;/a&gt; para que una prueba no contamine otra. Es una de esas mejoras que no lucen en demo, pero bajan bastante la fricción del día a día.&lt;/p&gt;

&lt;p&gt;Si tu equipo usa un &lt;code&gt;correo temporal desechable&lt;/code&gt; para QA, úsalo como apoyo de verificación y no como centro del diseño. El valor real sigue estando en los estados, los timestamps y la capacidad de reenviar de forma segura. En notas internas he visto cosas escritas como &lt;code&gt;tempail&lt;/code&gt; o &lt;code&gt;tamp mail com&lt;/code&gt;, y justo por eso prefiero que el sistema dependa menos de memoria humana y más de evidencia operativa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes en SaaS pequenos
&lt;/h2&gt;

&lt;p&gt;Estos fallos aparecen muchísimo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no guardar un identificador del envío&lt;/li&gt;
&lt;li&gt;tratar "aceptado por proveedor" como si fuera "usuario activado"&lt;/li&gt;
&lt;li&gt;no poner límite o contexto al botón de reenviar&lt;/li&gt;
&lt;li&gt;esconder el motivo del fallo solo en logs&lt;/li&gt;
&lt;li&gt;mezclar métricas de registro con métricas de activación&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un marco útil aquí es DORA: equipos con ciclos de feedback más cortos suelen recuperarse mejor de fallos operativos &lt;a href="https://cloud.google.com/devops/state-of-devops" rel="noopener noreferrer"&gt;source&lt;/a&gt;. No es una investigación sobre onboarding por email en específico, pero el principio aplica bastante bien. Cuando el equipo ve el atasco antes, lo corrige antes. Es casi obvio, si, pero no siempre se implementa.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  ¿Hace falta mostrar todos los estados al usuario?
&lt;/h3&gt;

&lt;p&gt;No. Muchas veces basta con mostrar un mensaje claro y reservar el detalle para soporte o un panel interno. Lo importante es que alguien del equipo pueda ver la verdad sin abrir cinco herramientas.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué conviene hacer primero?
&lt;/h3&gt;

&lt;p&gt;Yo empezaría por modelar estados y guardar transiciones. Después vendrán mejores copys, automatizaciones y alertas. Sin esa base, todo lo demás queda medio flojo.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Esto sirve si el volumen todavía es bajo?
&lt;/h3&gt;

&lt;p&gt;Sí, incluso más. En equipos chicos cada ticket duele más y cada duda consume atención cara. Un flujo ordenado desde temprano evita remiendos despues.&lt;/p&gt;

&lt;p&gt;En resumen, mejorar activación por email en un SaaS no suele requerir magia. Requiere visibilidad, pasos simples y un lenguaje común entre soporte, producto y backend. Cuando eso aparece, el onboarding deja de sentirse fragil y empieza a volverse operable de verdad.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>backend</category>
      <category>productivity</category>
      <category>marketing</category>
    </item>
    <item>
      <title>SaaS: activa usuarios sin mezclar senales</title>
      <dc:creator>Hannah</dc:creator>
      <pubDate>Mon, 31 Aug 2026 11:23:52 +0000</pubDate>
      <link>https://dev.to/hannahdev56/saas-activa-usuarios-sin-mezclar-senales-5829</link>
      <guid>https://dev.to/hannahdev56/saas-activa-usuarios-sin-mezclar-senales-5829</guid>
      <description>&lt;p&gt;En SaaS pequeno, una de las primeras trampas no aparece en el codigo. Aparece en la lectura. Crees que una mejora de onboarding funciono porque subio la activacion, pero luego descubres que medio equipo estuvo probando el flujo el mismo dia. La subida era real, si, pero no significaba lo que pensabas.&lt;/p&gt;

&lt;p&gt;Me gusta arreglar esto con una idea muy simple: definir activacion con una señal que producto, Backend y growth puedan leer igual. No es un sistema enorme. Es mas bien una base ordenada para que tus experimentos no se vuelvan confeti.&lt;/p&gt;

&lt;p&gt;Si ya vienes trabajando cohortes, este paso encaja muy bien con estas &lt;a href="https://dev.to/hannahdev56/saas-cohortes-de-onboarding-sin-ruido-30nk"&gt;cohortes de onboarding sin ruido&lt;/a&gt;. La diferencia ahora es que no solo separas grupos: tambien decides que evento cuenta como progreso real.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: buenas activaciones, lectura mala
&lt;/h2&gt;

&lt;p&gt;Muchos equipos empiezan midiendo algo facil:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;usuario creado&lt;/li&gt;
&lt;li&gt;email abierto&lt;/li&gt;
&lt;li&gt;primer login&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No esta mal para arrancar, pero casi nunca alcanza. Un usuario creado puede ser una prueba interna. Un email abierto puede venir de QA. Un primer login puede pasar porque alguien hizo una demo. Todo eso mete ruido, y el dashboard se ve mejor de lo que el negocio se siente de verdad.&lt;/p&gt;

&lt;p&gt;Para Startups en etapa temprana, yo prefiero una pregunta mas concreta: "¿que hizo esta persona que nos dice que entendio el valor inicial del producto?". A veces es crear un workspace. Otras veces es importar datos, invitar a un colega o terminar la configuracion base. Esa señal debe ser corta, observable y repetible. Si cambias la definicion cada semana, luego nadie confia mucho en la metrica.&lt;/p&gt;

&lt;p&gt;Tambien ayuda recordar que una señal util no siempre es la mas obvia. Aveces el mejor evento no es el primer click, sino el primer paso que cuesta un poquito mas y por eso separa curiosidad de interes real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una definicion simple de activacion para SaaS pequeno
&lt;/h2&gt;

&lt;p&gt;Este esquema funciona bastante bien cuando el equipo aun esta creciendo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;define una sola activacion principal por flujo&lt;/li&gt;
&lt;li&gt;marca si la cuenta es &lt;code&gt;internal-test&lt;/code&gt;, &lt;code&gt;beta&lt;/code&gt; o &lt;code&gt;trial&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;guarda de donde vino el usuario&lt;/li&gt;
&lt;li&gt;registra el momento exacto del evento de activacion&lt;/li&gt;
&lt;li&gt;revisa el resultado por cohorte, no solo en total&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Por ejemplo, para una herramienta B2B pequena, la activacion puede ser "crear el primer tablero con datos reales". Para otra, puede ser "invitar a un segundo usuario". No necesitas veinte reglas. Necesitas una regla que el equipo pueda explicar sin pelearse en Slack.&lt;/p&gt;

&lt;p&gt;Cuando alguien nuevo me pregunta por esto, tambien le digo algo medio aburrido pero util: no mezcles chequeos operativos con avance del usuario. Si estas validando entregabilidad, accesos o un dummy e mail durante staging, eso no deberia terminar contando como progreso de activacion. Parece obvio, pero pasa todo el tiempo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Que guardar en Backend para no discutir despues
&lt;/h2&gt;

&lt;p&gt;La parte bonita es que el modelo puede ser pequeno. Algo asi ya resuelve bastante:&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;ActivationRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userId&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;cohort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;internal-test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;beta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trial&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;organic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sales&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;activationEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;workspace_created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data_imported&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;teammate_invited&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;activatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isQualifiedActivation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ActivationRecord&lt;/span&gt;&lt;span class="p"&gt;)&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;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cohort&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;internal-test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activatedAt&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&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;Lo importante no es TypeScript. Lo importante es que luego puedas responder preguntas sencillas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿subio la activacion real o solo subieron las pruebas?&lt;/li&gt;
&lt;li&gt;¿una fuente convierte mejor que otra?&lt;/li&gt;
&lt;li&gt;¿cambio el onboarding o cambio la mezcla de usuarios?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese tipo de claridad recuerda mucho a trabajar con &lt;a href="https://dev.to/alexcarteruk/kubernetes-alertas-utiles-para-readiness-flapping-1bi1"&gt;alertas utiles y menos ruido operativo&lt;/a&gt;. En los dos casos intentas que la señal correcta llegue rapido y que el equipo no persiga falsos positivos todo el rato.&lt;/p&gt;

&lt;p&gt;Si quieres ir un paso mas alla, añade un &lt;code&gt;experiment_id&lt;/code&gt; o &lt;code&gt;batch_id&lt;/code&gt;. No hace falta al principio, pero cuando haces dos cambios de onboarding en la misma semana, ese dato te ahorra una discusion un poco tonta despues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes en experimentos de onboarding
&lt;/h2&gt;

&lt;p&gt;El error numero uno es medir solo en agregado. Si el total mejora pero la cohorte &lt;code&gt;trial&lt;/code&gt; cae, algo raro paso. El total puede esconder una mezcla fea de trafico, pruebas internas o leads de poca calidad.&lt;/p&gt;

&lt;p&gt;El segundo error es usar demasiadas señales al mismo tiempo. Algunos tableros tienen apertura, click, login, tiempo en pagina, invitaciones y uso de feature, todo junto, desde el dia uno. Eso abruma bastante. Para equipos pequenos, una señal principal y una secundaria suele ser mas que suficiente.&lt;/p&gt;

&lt;p&gt;El tercer error es esperar "la arquitectura perfecta". Yo no esperaria. En serio. Un registro claro hoy vale mas que una promesa de tracking impecable dentro de tres meses. La instrumentacion minima bien pensada gana muchas veces.&lt;/p&gt;

&lt;p&gt;Mi mini checklist seria esta:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;una definicion de activacion por flujo&lt;/li&gt;
&lt;li&gt;cohortes visibles en la base de datos&lt;/li&gt;
&lt;li&gt;exclusiones claras para pruebas internas&lt;/li&gt;
&lt;li&gt;una revision semanal de resultados&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No suena glamoroso, lo se. Pero hace que marketing, producto y Backend hablen de lo mismo. Y cuando eso pasa, las decisiones salen mas rapido y con menos humo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A rapido
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Tengo que cambiar la activacion cuando cambia el producto?
&lt;/h3&gt;

&lt;p&gt;Si, pero con cuidado. Cambiala cuando el valor inicial del producto cambie de verdad, no cuando quieras que la grafica se vea mejor. Deja una nota simple de desde cuando aplica la nueva definicion.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Esto solo sirve para productos con signup por email?
&lt;/h3&gt;

&lt;p&gt;No. Sirve para cualquier onboarding donde quieras separar curiosidad, prueba y uso real. El email suele ser el punto visible, pero la leccion de fondo es sobre senales y contexto.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cuanto detalle necesito al principio?
&lt;/h3&gt;

&lt;p&gt;Menos del que imaginas. Una buena señal, tres cohortes y una exclusión para pruebas internas ya te dejan trabajar bastante bien. Luego iteras. No perfecto, pero si mucho mas util.&lt;/p&gt;

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