A few months ago I shipped VanishInbox, 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.
This is a writeup of the architecture choices, the tradeoffs I made, and a few things I'd do differently.
The core constraint: no persistence
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.
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.
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.
Inbound routing: Cloudflare Email Routing + Workers
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.
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 ReadableStream via the email event handler, and you parse it with postal-mime to extract headers, body, and attachments.
export default {
async email(message, env, ctx) {
const parser = new PostalMime();
const email = await parser.parse(message.raw);
// write to Redis with TTL
}
}
The catch-all is important. I don't pre-provision addresses. Any anything@vanishinbox.com 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.
The frontend polling problem
Once an email lands in Redis, the user needs to see it. There are two obvious approaches: WebSockets or polling. I went with polling.
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.
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.
What I deliberately left out
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.
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.
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.
The privacy page problem
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.
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.
What I'd change
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.
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.
The part that took longest
Inbound email parsing. postal-mime 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.
If you're building anything that ingests arbitrary email, budget more time here than you expect.
The full service is at vanishinbox.com if you want to see the end result. Happy to answer questions about any of the implementation details in the comments.
Top comments (0)