<?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: Alex K</title>
    <description>The latest articles on DEV Community by Alex K (@vanishinbox).</description>
    <link>https://dev.to/vanishinbox</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%2F4007715%2F858b591d-ce5c-45e5-883c-bc2dc8fa7c17.jpg</url>
      <title>DEV Community: Alex K</title>
      <link>https://dev.to/vanishinbox</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vanishinbox"/>
    <language>en</language>
    <item>
      <title>I Built a Disposable Email Service on Cloudflare Workers + Upstash Redis. Here's the Architecture.</title>
      <dc:creator>Alex K</dc:creator>
      <pubDate>Wed, 15 Jul 2026 14:27:33 +0000</pubDate>
      <link>https://dev.to/vanishinbox/i-built-a-disposable-email-service-on-cloudflare-workers-upstash-redis-heres-the-architecture-2a6j</link>
      <guid>https://dev.to/vanishinbox/i-built-a-disposable-email-service-on-cloudflare-workers-upstash-redis-heres-the-architecture-2a6j</guid>
      <description>&lt;h2&gt;
  
  
  I Built a Disposable Email Service on Cloudflare Workers + Upstash Redis. Here's the Architecture.
&lt;/h2&gt;

&lt;p&gt;I run &lt;a href="https://vanishinbox.com" rel="noopener noreferrer"&gt;VanishInbox&lt;/a&gt;, a free disposable email service. No signup, no backend server in the traditional sense, no database in the traditional sense either. Just Cloudflare and Redis doing the work. Here's how it's put together.&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem
&lt;/h3&gt;

&lt;p&gt;Disposable email sounds trivial until you sit down to build it. You need an address that receives real mail from real mail servers, stores it somewhere, and then deletes itself without anyone lifting a finger. Three requirements pull in different directions: you need to accept SMTP traffic, you need fast reads for the inbox UI, and you need guaranteed deletion without running a cron job that might fail silently at 3am and leave stale data sitting around.&lt;/p&gt;

&lt;p&gt;Most tutorials on "build a temp mail service" skip straight to a database schema. That's the wrong starting point. The interesting part is the deletion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Redis TTL instead of a cleanup job
&lt;/h3&gt;

&lt;p&gt;I store every inbound email in Upstash Redis with a native TTL of 10 minutes. When the TTL expires, Redis removes the key itself. No worker scanning for expired rows, no scheduled job, no risk of a cron trigger getting skipped and leaving orphaned data behind.&lt;/p&gt;

&lt;p&gt;This was a deliberate tradeoff. A cron-based approach gives you more control (you can log deletions, run analytics before purging, batch things up), but it adds a moving part that can fail. With TTL, the expiry is a property of the data itself. The email can't outlive its own storage layer even if every other part of the stack goes down.&lt;/p&gt;

&lt;p&gt;The downside is you lose the ability to do anything clever at deletion time. I decided that's fine. The entire value proposition is that the data is gone. Building tooling to intercept its own disappearance would work against the point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Receiving mail without running a mail server
&lt;/h3&gt;

&lt;p&gt;This is the part people usually assume needs a dedicated SMTP server sitting somewhere. It doesn't.&lt;/p&gt;

&lt;p&gt;Cloudflare Email Routing sits in front of the domain and catches inbound mail at the DNS/MX level. Instead of forwarding it to a real inbox, I route it to a Cloudflare Worker. The Worker receives the raw message, parses the MIME content (headers, plaintext body, HTML body, attachments), and writes the parsed result into Redis under a key derived from the generated inbox address.&lt;/p&gt;

&lt;p&gt;The frontend polls that same key. When a message lands, it shows up in the inbox UI within a couple of seconds. There's no queue, no message broker, no server process that has to stay alive between requests. The Worker only runs when mail actually arrives.&lt;/p&gt;

&lt;p&gt;This is the architecture I'd point to if someone asked what "edge computing" actually buys you in practice. You get a mail-receiving service with zero idle compute cost and zero server to patch or maintain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why 10 minutes specifically
&lt;/h3&gt;

&lt;p&gt;The number isn't arbitrary. It needs to be long enough to receive a signup confirmation or verification code without the user racing a countdown, and short enough that the address doesn't become a semi-permanent throwaway account that ends up on spam lists or gets scraped and indexed somewhere. 10 minutes covers almost every real verification flow while keeping the inbox's useful life short enough that abuse has a low ceiling.&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem nobody warns you about: bot traffic
&lt;/h3&gt;

&lt;p&gt;Once the service was live, the biggest engineering problem wasn't email parsing. It was bots hammering the inbox API to generate addresses at scale for their own automation.&lt;/p&gt;

&lt;p&gt;Core Web Vitals data doesn't help here. A headless browser running Playwright generates the same paint and interaction metrics a real user does, so you can't use performance telemetry to separate bots from people. I ended up deploying a Cloudflare Turnstile invisible check across the tool pages, a signed short-lived cookie proving a real verification pass, and a rate limit on the inbox API endpoint (20 requests per 10 seconds triggers a managed challenge). A couple of high-volume IPs still got blocked directly because they ignored all of it and kept hitting the API in a loop.&lt;/p&gt;

&lt;p&gt;None of that shows up in an architecture diagram, but it's most of the ongoing maintenance work.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd do differently
&lt;/h3&gt;

&lt;p&gt;I'd build the rate limiting and bot detection at the same time as the core service, not months after launch. Ephemeral, free, no-signup tools are exactly the kind of target automation goes after first, since there's no account to ban and no friction to slow it down.&lt;/p&gt;

&lt;p&gt;If you want to see the whole thing running, the tool itself is live at &lt;a href="https://vanishinbox.com/10-minute-email" rel="noopener noreferrer"&gt;vanishinbox.com/10-minute-email&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Building a Privacy-First Disposable Email Service: Architecture Decisions and Lessons Learned</title>
      <dc:creator>Alex K</dc:creator>
      <pubDate>Mon, 29 Jun 2026 09:19:39 +0000</pubDate>
      <link>https://dev.to/vanishinbox/building-a-privacy-first-disposable-email-service-architecture-decisions-and-lessons-learned-4434</link>
      <guid>https://dev.to/vanishinbox/building-a-privacy-first-disposable-email-service-architecture-decisions-and-lessons-learned-4434</guid>
      <description>&lt;p&gt;A few months ago I shipped &lt;a href="https://vanishinbox.com" rel="noopener noreferrer"&gt;VanishInbox&lt;/a&gt;, a free disposable email service. No accounts, no history, emails gone after 10 minutes. The concept is simple. The decisions underneath it weren't always obvious.&lt;/p&gt;

&lt;p&gt;This is a writeup of the architecture choices, the tradeoffs I made, and a few things I'd do differently.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core constraint: no persistence
&lt;/h2&gt;

&lt;p&gt;Most email services are built around storage. You receive a message, it sits in a database until you delete it. I wanted the opposite: receive a message, surface it, then delete it automatically with no manual step required.&lt;/p&gt;

&lt;p&gt;Upstash Redis with a TTL handles this cleanly. Every inbound email gets written to a key with a 10-minute expiry. Redis handles the deletion. I don't run a cron job, I don't write cleanup logic, the data just evaporates. For a privacy-focused product this matters more than it might seem — if you're not storing data, you can't leak it, and you can't be compelled to hand it over.&lt;/p&gt;

&lt;p&gt;The tradeoff is that you can't offer any kind of history, search, or "I accidentally closed the tab" recovery. I decided that was fine. The use case is throwaway signups and one-time confirmations, not email management.&lt;/p&gt;




&lt;h2&gt;
  
  
  Inbound routing: Cloudflare Email Routing + Workers
&lt;/h2&gt;

&lt;p&gt;Getting email into a web app is the part most tutorials skip. You need an MX record pointing somewhere, something to receive SMTP, and a way to parse the raw message into something usable.&lt;/p&gt;

&lt;p&gt;Cloudflare Email Routing lets you set a catch-all rule on your domain and forward matching addresses to a Worker. The Worker receives the raw email as a &lt;code&gt;ReadableStream&lt;/code&gt; via the &lt;code&gt;email&lt;/code&gt; event handler, and you parse it with &lt;code&gt;postal-mime&lt;/code&gt; to extract headers, body, and attachments.&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;email&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&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;parser&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;PostalMime&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;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// write to Redis with TTL&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;The catch-all is important. I don't pre-provision addresses. Any &lt;code&gt;anything@vanishinbox.com&lt;/code&gt; address exists the moment someone types it into the inbox field and presses enter. The Worker receives whatever arrives there and writes it keyed by the local part. If nothing arrives, the address never existed in any meaningful sense.&lt;/p&gt;




&lt;h2&gt;
  
  
  The frontend polling problem
&lt;/h2&gt;

&lt;p&gt;Once an email lands in Redis, the user needs to see it. There are two obvious approaches: WebSockets or polling. I went with polling.&lt;/p&gt;

&lt;p&gt;WebSockets on Cloudflare Workers require Durable Objects, which adds complexity and cost. For a free service where the inbox session lasts 10 minutes, polling every 5 seconds is fine. The payload is tiny, the requests are cheap, and it keeps the architecture flat.&lt;/p&gt;

&lt;p&gt;Next.js API routes hit Redis on each poll. Response time is fast enough that users don't notice they're not getting a push.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I deliberately left out
&lt;/h2&gt;

&lt;p&gt;No accounts. Accounts mean a user table, password resets, session management, and a support queue for people who lose access. More importantly, accounts mean I know who you are. The whole point of a disposable inbox is that you use it without identifying yourself. An account requirement undermines that.&lt;/p&gt;

&lt;p&gt;No attachment storage beyond the session. Attachments get held in memory during parsing and included in the Redis payload if they're small enough. Large attachments get dropped. This is a deliberate limit, not an oversight.&lt;/p&gt;

&lt;p&gt;No sender verification. I accept mail from anywhere. SPF, DKIM, and DMARC checks happen at Cloudflare's level before the Worker sees the message, but I don't reject on failure. The inbox is throwaway by design, so the risk model is different from a real mailbox.&lt;/p&gt;




&lt;h2&gt;
  
  
  The privacy page problem
&lt;/h2&gt;

&lt;p&gt;A disposable email service needs a privacy policy that's actually accurate, not just boilerplate. The usual template language about "we retain your data for 90 days" doesn't apply when retention is 10 minutes by design.&lt;/p&gt;

&lt;p&gt;I ended up writing mine to explicitly state what gets stored (the email payload), for how long (10 minutes), and what doesn't get stored (IP addresses, browser fingerprints, who accessed which inbox). Termly generated the base document and I edited the retention section to reflect reality.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd change
&lt;/h2&gt;

&lt;p&gt;The 10-minute TTL was an arbitrary starting point. In practice, some confirmation emails take 3-4 minutes to arrive, which leaves a narrow window. A 15 or 20-minute default would reduce friction without meaningfully weakening the privacy model.&lt;/p&gt;

&lt;p&gt;I'd also look at Durable Objects earlier for the polling layer. The current polling approach works but a persistent connection per inbox would let me push updates rather than pull them, which would feel snappier and reduce Redis reads.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part that took longest
&lt;/h2&gt;

&lt;p&gt;Inbound email parsing. &lt;code&gt;postal-mime&lt;/code&gt; handles most cases well, but real-world emails are messy. Multipart messages with nested boundaries, HTML-only bodies with no text fallback, character encoding edge cases. Getting the rendered body to look reasonable across common senders took more iteration than the rest of the project combined.&lt;/p&gt;

&lt;p&gt;If you're building anything that ingests arbitrary email, budget more time here than you expect.&lt;/p&gt;




&lt;p&gt;The full service is at &lt;a href="https://vanishinbox.com" rel="noopener noreferrer"&gt;vanishinbox.com&lt;/a&gt; if you want to see the end result. Happy to answer questions about any of the implementation details in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cloudflare</category>
      <category>redis</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
