<?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: zerodrop</title>
    <description>The latest articles on DEV Community by zerodrop (@zerodrop).</description>
    <link>https://dev.to/zerodrop</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3963379%2Fffcb1c8f-729c-4777-8c14-e3cccac1774b.png</url>
      <title>DEV Community: zerodrop</title>
      <link>https://dev.to/zerodrop</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zerodrop"/>
    <language>en</language>
    <item>
      <title>I built a disposable email service that costs $0 to run — here's the stack and the real numbers</title>
      <dc:creator>zerodrop</dc:creator>
      <pubDate>Mon, 01 Jun 2026 22:52:12 +0000</pubDate>
      <link>https://dev.to/zerodrop/i-built-a-disposable-email-service-that-costs-0-to-run-heres-the-stack-and-the-real-numbers-5d6i</link>
      <guid>https://dev.to/zerodrop/i-built-a-disposable-email-service-that-costs-0-to-run-heres-the-stack-and-the-real-numbers-5d6i</guid>
      <description>&lt;p&gt;Six weeks ago I was building an auth flow and needed a throwaway inbox to test if the password reset email actually arrived. I opened a temp email site, navigated past three ad banners, got a random address, and waited.&lt;/p&gt;

&lt;p&gt;That workflow was broken enough that I built the infrastructure myself instead.&lt;/p&gt;

&lt;p&gt;Here's what I built, what it cost, and what happened after.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem in one sentence
&lt;/h2&gt;

&lt;p&gt;Every developer testing email flows either mocks the email (wrong), runs a local SMTP server (overkill), or uses a throwaway email site with ads (terrible DX). None of these are good answers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack — chosen for $0 idle cost
&lt;/h2&gt;

&lt;p&gt;Every service in the stack was chosen with one constraint: it must cost nothing at zero traffic and scale automatically when traffic arrives.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Email interception&lt;/td&gt;
&lt;td&gt;Cloudflare Email Routing&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serverless logic&lt;/td&gt;
&lt;td&gt;Cloudflare Workers&lt;/td&gt;
&lt;td&gt;100k req/day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI spam filter&lt;/td&gt;
&lt;td&gt;Cloudflare Workers AI&lt;/td&gt;
&lt;td&gt;Generous&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Upstash Redis&lt;/td&gt;
&lt;td&gt;10k commands/day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Next.js on Vercel&lt;/td&gt;
&lt;td&gt;Generous&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain (routing)&lt;/td&gt;
&lt;td&gt;zerodrop-sandbox.online&lt;/td&gt;
&lt;td&gt;₹89/year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain (brand)&lt;/td&gt;
&lt;td&gt;zerodrop.dev&lt;/td&gt;
&lt;td&gt;₹1,500/year&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Total spend to launch: ₹1,589 (~$19)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's it. No servers, no VMs, no managed databases with minimum monthly fees. The entire thing scales to zero when nobody is using it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture in plain English
&lt;/h2&gt;

&lt;p&gt;An email arrives at &lt;code&gt;anything@zerodrop-sandbox.online&lt;/code&gt;. Cloudflare intercepts it at the MX level and triggers a Worker function. The Worker runs a Llama 3 classification — if it's spam, it gets dropped silently. If it's legitimate, it gets parsed into clean JSON and pushed to Upstash Redis with a 30-minute TTL. The Next.js dashboard polls every 3 seconds and displays it instantly.&lt;/p&gt;

&lt;p&gt;The whole thing processes an email in under 500ms from arrival to display.&lt;/p&gt;




&lt;h2&gt;
  
  
  The two-domain decision
&lt;/h2&gt;

&lt;p&gt;The routing domain (&lt;code&gt;zerodrop-sandbox.online&lt;/code&gt;) and the brand domain (&lt;code&gt;zerodrop.dev&lt;/code&gt;) are intentionally separate. Disposable email domains get blocklisted. When that happens — and it will — you replace the routing domain for ₹89 and update one DNS record. The brand domain, the dashboard, the npm package, the users' bookmarks — none of it is affected.&lt;/p&gt;

&lt;p&gt;This was a $10 architectural decision that prevents a future crisis.&lt;/p&gt;




&lt;h2&gt;
  
  
  The numbers after launch
&lt;/h2&gt;

&lt;p&gt;These are real, unmanipulated numbers from the first week with zero paid promotion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;338 npm installs&lt;/strong&gt; of &lt;code&gt;zerodrop-client&lt;/code&gt; — organic, from search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 confirmed organic user&lt;/strong&gt; — visible in Upstash usage data. Someone found the site, generated an inbox, received a real email, and read it. Before any marketing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6,300+ Redis commands&lt;/strong&gt; in the first 72 hours — 
and climbing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$0.00&lt;/strong&gt; infrastructure cost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;₹1,589&lt;/strong&gt; total spend including domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 338 npm installs before any promotion is the signal that matters. Developers are actively searching for this solution and finding it through keyword discovery alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's coming next
&lt;/h2&gt;

&lt;p&gt;The free tier works. The next milestone is the Workspace tier — custom domain routing, team seats, API keys, webhook support for CI pipelines. The pricing is $49/month per workspace.&lt;/p&gt;

&lt;p&gt;The waitlist is open at zerodrop.dev. When 25-30 signups come from company email addresses, that's when the Workspace tier gets built. Until then the free tier runs itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  The honest assessment
&lt;/h2&gt;

&lt;p&gt;This is not a finished product. It's a free tier with a waitlist and a hypothesis: that QA teams at companies will pay $49/month for custom domain email testing infrastructure embedded in their CI pipelines.&lt;/p&gt;

&lt;p&gt;The hypothesis is unproven. The free tier is real, the organic usage is real, and the npm downloads are real. Everything else is still a bet.&lt;/p&gt;

&lt;p&gt;If you're building something similar or have feedback on the architecture — I'm at the &lt;a href="https://x.com/zerodropdev" rel="noopener noreferrer"&gt;@zerodropdev&lt;/a&gt; handle on X.&lt;/p&gt;




&lt;h2&gt;
  
  
  The SDK
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;zerodrop-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ZeroDrop&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zerodrop-client&lt;/span&gt;&lt;span class="dl"&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;mail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ZeroDrop&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;inbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateInbox&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;email&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;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ &lt;a href="https://zerodrop.dev" rel="noopener noreferrer"&gt;zerodrop.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>cloudflare</category>
      <category>indiehacker</category>
    </item>
    <item>
      <title>Testing email flows in Playwright without a mail server</title>
      <dc:creator>zerodrop</dc:creator>
      <pubDate>Mon, 01 Jun 2026 22:24:39 +0000</pubDate>
      <link>https://dev.to/zerodrop/testing-email-flows-in-playwright-without-a-mail-server-2ll7</link>
      <guid>https://dev.to/zerodrop/testing-email-flows-in-playwright-without-a-mail-server-2ll7</guid>
      <description>&lt;p&gt;Every QA engineer has written a test like this at some point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user receives verification email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&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="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/signup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="email"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[type="submit"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 🤔 now what?&lt;/span&gt;
  &lt;span class="c1"&gt;// mock the email? skip the assertion?&lt;/span&gt;
  &lt;span class="c1"&gt;// hope it works in production?&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verification email step gets skipped, mocked, or marked as "manual test only." The auth flow ships without end-to-end coverage. Six months later a misconfigured SendGrid template breaks signup and nobody catches it until users complain.&lt;/p&gt;

&lt;p&gt;This is a solved problem. Here's how to test it properly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why mocking email is the wrong approach
&lt;/h2&gt;

&lt;p&gt;Mocking your email provider in tests gives you confidence that your code calls &lt;code&gt;sendEmail()&lt;/code&gt; — not that the email actually arrives, renders correctly, contains the right link, or doesn't get flagged as spam.&lt;/p&gt;

&lt;p&gt;The things that actually break in production are never the things you mocked. They're the SendGrid template that got corrupted, the verification URL that points to staging instead of production, the email that arrives 45 seconds late and times out the user's session.&lt;/p&gt;

&lt;p&gt;Real tests require real emails.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why running a mail server is overkill
&lt;/h2&gt;

&lt;p&gt;The traditional answer is to run a local SMTP server — Mailhog, Mailtrap, or Mailpit — in your test environment. This works but introduces real complexity:&lt;/p&gt;

&lt;p&gt;You need the mail server running before your tests. In CI that means a Docker container, a service dependency, a health check, and a teardown step. Your test suite now has an infrastructure dependency that can fail independently of your application code.&lt;/p&gt;

&lt;p&gt;For most teams this is more complexity than the problem warrants. You don't need a mail server. You need an inbox you can read from inside a test.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern: real inboxes, real assertions
&lt;/h2&gt;

&lt;p&gt;The correct abstraction is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a unique email address per test run&lt;/li&gt;
&lt;li&gt;Use that address in your test flow&lt;/li&gt;
&lt;li&gt;Wait for the email to arrive&lt;/li&gt;
&lt;li&gt;Assert on the actual content&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No SMTP server. No Docker dependency. No infrastructure to maintain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ZeroDrop&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zerodrop-client&lt;/span&gt;&lt;span class="dl"&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;mail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ZeroDrop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user receives password reset email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&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="c1"&gt;// Generate a unique inbox for this test run&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateInbox&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Use it in your app flow&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/forgot-password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="email"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inbox&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;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[type="submit"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Wait for the actual email to arrive&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&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;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Assert on real content&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;email&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;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reset your password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&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;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Click here to reset&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Extract the actual reset link and follow it&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resetLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/https&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\/\/[^\s]&lt;/span&gt;&lt;span class="sr"&gt;+reset&lt;/span&gt;&lt;span class="se"&gt;[^\s]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&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;resetLink&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&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;resetLink&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Assert you landed on the reset page&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;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/reset-password/&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;That test covers the entire flow end to end — form submission, email delivery, link extraction, and the destination page. No mocks anywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  The timeout error matters
&lt;/h2&gt;

&lt;p&gt;Notice &lt;code&gt;waitForLatest&lt;/code&gt; throws a &lt;code&gt;ZeroDropTimeoutError&lt;/code&gt; if the email never arrives within the timeout. That's intentional and important.&lt;/p&gt;

&lt;p&gt;A test that times out and throws is a test that fails loudly. Your CI pipeline goes red. Someone investigates. They find that SendGrid stopped delivering because of a misconfigured API key.&lt;/p&gt;

&lt;p&gt;Compare that to a test that catches the timeout, returns null, and passes silently. The broken email flow ships to production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ZeroDrop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ZeroDropTimeoutError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zerodrop-client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;try&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;email&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;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt; &lt;span class="p"&gt;});&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;email&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;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reset your password&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;ZeroDropTimeoutError&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;`Email never arrived at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — check your email provider config`&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="nx"&gt;err&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;Explicit failures with useful error messages are what separate a test suite from a false confidence generator.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cypress integration
&lt;/h2&gt;

&lt;p&gt;The same pattern works in Cypress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ZeroDrop&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zerodrop-client&lt;/span&gt;&lt;span class="dl"&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;mail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ZeroDrop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email verification flow&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sends verification email on signup&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="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;inbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateInbox&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/signup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[name="email"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[type="submit"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrap&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="nf"&gt;waitForLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;email&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&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="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Verify your email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&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;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verify&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="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  CI/CD integration
&lt;/h2&gt;

&lt;p&gt;In GitHub Actions the setup is zero — no services block, no Docker, no health checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run E2E tests&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ZERODROP_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ZERODROP_API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire email testing infrastructure. One environment variable.&lt;/p&gt;

&lt;p&gt;Compare to a Mailhog setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mailhog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mailhog/mailhog&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;1025:1025&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;8025:8025&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for Mailhog&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sleep &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run E2E tests&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;SMTP_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost&lt;/span&gt;
    &lt;span class="na"&gt;SMTP_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1025&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both work. One requires zero infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The free tier is enough for most teams
&lt;/h2&gt;

&lt;p&gt;For local development and smaller test suites, the zero-auth mode requires no API key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// No API key — uses public sandbox&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;new&lt;/span&gt; &lt;span class="nc"&gt;ZeroDrop&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;inbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateInbox&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inboxes expire after 30 minutes and emails go through AI spam filtering. For CI pipelines that need custom domains, guaranteed delivery, and longer retention — that's what the Workspace tier is for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;zerodrop-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try it on your next auth flow test. The first email that arrives in a real inbox inside a Playwright test is a satisfying moment.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How I built a zero-cost edge email pipeline using Cloudflare Workers</title>
      <dc:creator>zerodrop</dc:creator>
      <pubDate>Mon, 01 Jun 2026 21:11:54 +0000</pubDate>
      <link>https://dev.to/zerodrop/how-i-built-a-zero-cost-edge-email-pipeline-using-cloudflare-workers-27kp</link>
      <guid>https://dev.to/zerodrop/how-i-built-a-zero-cost-edge-email-pipeline-using-cloudflare-workers-27kp</guid>
      <description>&lt;p&gt;Every developer has hit this wall. You're building a password reset flow or an email verification step and you need a throwaway inbox to confirm the email actually arrives. Every existing tool either shows you ads, requires an account, or has an interface that looks like it was built in 2009.&lt;/p&gt;

&lt;p&gt;So I built the infrastructure myself. Here's the architecture — and why each decision was made.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core insight: email is just HTTP at the edge
&lt;/h2&gt;

&lt;p&gt;Modern serverless platforms have made it possible to intercept inbound email the same way you'd intercept an HTTP request — with a lightweight function that runs at the edge, costs nothing at idle, and scales automatically.&lt;/p&gt;

&lt;p&gt;Cloudflare Email Routing is the entry point. You configure a catch-all rule on a domain and every email sent to that domain — regardless of prefix — triggers a Worker function. &lt;code&gt;anything@yourdomain.com&lt;/code&gt;, &lt;code&gt;test-abc@yourdomain.com&lt;/code&gt;, &lt;code&gt;qa-pipeline-7@yourdomain.com&lt;/code&gt; — all caught, zero per-address configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-layer architecture
&lt;/h2&gt;

&lt;p&gt;The system has three components that each do exactly one thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The routing domain.&lt;/strong&gt; A cheap disposable domain — not your brand domain — that exists solely to catch email. The separation matters: if this domain ends up on a spam blocklist (and with disposable email services, it will eventually), you replace it for $10 without your main product going offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Worker.&lt;/strong&gt; When an email arrives, the Worker receives a message object containing the sender, recipient, headers, and the full raw MIME body as a readable stream. The Worker's job is simple: parse the relevant fields into a clean JSON structure and decide whether to store or drop it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upstash Redis.&lt;/strong&gt; Emails are stored in a list keyed by inbox name — so &lt;code&gt;test-abc@yourdomain.com&lt;/code&gt; maps to the Redis key &lt;code&gt;inbox:test-abc&lt;/code&gt;. A TTL is set on every write. After 30 minutes the key expires automatically. No cleanup job, no stale data, no accumulating storage costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision that makes it economically viable: edge AI filtering
&lt;/h2&gt;

&lt;p&gt;Without filtering, a routing domain gets discovered by bot farms within weeks of launch. They send thousands of automated emails — account farming scripts, scrapers, newsletter blasts. Each one costs a Redis write. At scale that burns through your free tier quota fast.&lt;/p&gt;

&lt;p&gt;The fix is running a lightweight classification step inside the Worker before any database write happens. Using Cloudflare Workers AI with Llama 3, you can classify each email's sender and subject line in milliseconds. If the model flags it as automated spam, the Worker returns early — the email is silently dropped and never touches Redis.&lt;/p&gt;

&lt;p&gt;The economics are stark. Cloudflare Workers AI inference at this scale costs fractions of a cent. A Redis write that gets skipped costs nothing. The filter pays for itself immediately.&lt;/p&gt;

&lt;p&gt;The key architectural detail is the fallback: if the AI call fails for any reason, the email is allowed through. You never drop a legitimate developer test email because of an infrastructure error. Fidelity always wins over filtering.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the free tier actually costs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;th&gt;First cost appears at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Email Routing&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Workers&lt;/td&gt;
&lt;td&gt;100k req/day&lt;/td&gt;
&lt;td&gt;~$5/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Workers AI&lt;/td&gt;
&lt;td&gt;Generous free tier&lt;/td&gt;
&lt;td&gt;High volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upstash Redis&lt;/td&gt;
&lt;td&gt;10k commands/day&lt;/td&gt;
&lt;td&gt;$0.2/100k commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel&lt;/td&gt;
&lt;td&gt;Generous free tier&lt;/td&gt;
&lt;td&gt;$20/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At zero traffic the monthly bill is zero. The first real cost appears when you're processing tens of thousands of emails daily — at which point you have a real product with real revenue to cover it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-domain strategy
&lt;/h2&gt;

&lt;p&gt;One architectural detail worth highlighting separately: never use your brand domain for email routing.&lt;/p&gt;

&lt;p&gt;The routing domain exists to be burned. When it gets blocklisted — and it will — you update one DNS record and replace it with a fresh domain. Your brand domain, your dashboard, your users' bookmarks — none of it is affected.&lt;/p&gt;

&lt;p&gt;The brand domain is precious. The routing domain is disposable. Treat them accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SDK problem
&lt;/h2&gt;

&lt;p&gt;Once the pipeline works, the natural next step is making it usable in CI. A REST API is a specification. What QA engineers actually need is an abstraction that hides the polling loop, handles timeouts gracefully, and throws a meaningful error when an expected email never arrives.&lt;/p&gt;

&lt;p&gt;The difference between a tool and infrastructure is that abstraction layer. A three-line integration that works in Playwright and Cypress without the developer understanding what's happening underneath — that's what makes something get embedded in codebases and never removed.&lt;/p&gt;

&lt;p&gt;The specific design decision that matters: a timeout should throw a named error class, not return null. CI pipelines need explicit failures. A test that silently passes because &lt;code&gt;waitForEmail()&lt;/code&gt; returned null and the assertion was never reached is worse than no test at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built with this architecture
&lt;/h2&gt;

&lt;p&gt;After proving the pipeline worked I packaged it properly. The result is &lt;strong&gt;ZeroDrop&lt;/strong&gt; — a free, ad-free temporary email sandbox at &lt;a href="https://zerodrop.dev" rel="noopener noreferrer"&gt;zerodrop.dev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The npm package &lt;code&gt;zerodrop-client&lt;/code&gt; wraps the polling logic for CI pipelines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;zerodrop-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ZeroDrop&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zerodrop-client&lt;/span&gt;&lt;span class="dl"&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;mail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ZeroDrop&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;inbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateInbox&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Use inbox in your test...&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&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;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="p"&gt;});&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;email&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;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Reset your password&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;The free tier uses a shared routing domain with AI filtering enabled. For teams who need custom domains, private storage, and unfiltered delivery — Workspaces are in private beta at &lt;a href="https://zerodrop.dev" rel="noopener noreferrer"&gt;zerodrop.dev&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture in one sentence
&lt;/h2&gt;

&lt;p&gt;Edge email interception → AI classification at the Worker level → Redis storage with TTL → polling API → SDK abstraction. Each layer does one thing and costs nothing until you have real scale.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>webdev</category>
      <category>devops</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
