Developer Testing
How We Built a Receive-Only 10-Minute Email Inbox
The architecture behind 10 Minute Temp Email: two Cloudflare Workers, D1 metadata, private R2 storage, Email Routing, and a cookie-bound session with no outbound mail.
This site is a small, opinionated mail system: create an address in the browser, receive a message, lose the inbox in 10 minutes. I built it as two Cloudflare Workers plus D1 and R2 so the public website never has to parse raw SMTP, and so there is no code path that sends mail.
This article describes only what is in the running service. It is not a benchmark report, and it does not include secrets, internal keys, or steps that would make the inbox easier to abuse.
Why receive-only
A disposable inbox that can send is a spam cannon with extra steps. This product has:
- No compose UI
- No SMTP submission
- No reply or forward APIs
The only mail path is inbound: Cloudflare Email Routing delivers to a Worker that decides whether the recipient is an active mailbox. If not, the message is rejected with a generic failure. That keeps expired addresses from collecting mail and keeps the abuse surface smaller than a full mailbox.
Two Workers, two jobs
| Piece | Role |
|---|---|
| Web Worker (Astro on Cloudflare) | Pages, session cookie, JSON APIs to create/read/delete a mailbox |
| Mail ingest Worker | Email handler + a per-minute cleanup cron |
| D1 | Mailbox rows, message metadata, short-lived rate-limit rows |
| R2 | Private JSON bodies and allowed attachments |
| Email Routing | Accepts mail for mail.10mintempemail.com and hands it to ingest |
The website origin and the mail domain are separate on purpose. The site you browse is not the MX target.
Creating a mailbox
The create API is same-origin JSON. It checks the request origin, optionally verifies Cloudflare Turnstile when that flag is on, and applies a hashed-IP rate limit before inserting a row.
The address local-part is random. Collision retries exist so two visitors are unlikely to receive the same string. The Worker then sets an HttpOnly, SameSite=Lax cookie whose lifetime matches the mailbox TTL. The raw token is not stored. D1 keeps an HMAC hash so later requests can prove ownership without the database holding the secret in plaintext.
JSON responses do not include mailbox UUIDs or storage keys. The browser sees the email address, expiry, and TTL—enough to run the UI, not enough to guess another user’s inbox.
Receiving a message
On ingest the Worker:
- Parses the envelope recipient and requires the configured mail domain
- Loads the mailbox by local-part and checks that it is active and unexpired
- Rejects oversized messages and full mailboxes
- Parses MIME (postal-mime), sanitizes text and HTML, and stores only allowlisted attachments
- Deduplicates repeats of the same message
- Writes R2 objects, then D1 metadata (and rolls storage back if the database insert fails)
Unknown recipients get a generic rejection. That is deliberate: the system should not confirm whether a random string “used to exist.”
Reading and deleting
The homepage React inbox polls a messages API. Every read authenticates the cookie, compares the token hash in constant time, and scopes SQL to that mailbox. Attachments are streamed from R2 through authenticated same-origin routes; object keys never go to the client.
Delete is an explicit API call that clears the session cookie. Expiry is the same outcome on a timer. Either way, cleanup is what actually removes bytes.
Lessons that showed up in testing
- The clock starts at create. If QA opened the mailbox, then slowly filled a long form, the code arrived after expiry. The checklist in testing signup flows exists because that happened to me.
- HTML email is hostile. Layout CSS is useful; scripts, forms, and SVG are not. Sanitization is a second article.
- Cookies do not teleport. Opening the address on a phone after creating it on a laptop looks like “the product is broken.” It is a different session.
- Receive-only is a feature users notice. People look for Reply. Documenting the absence is better than shipping a send path.
What this article leaves out
I am not publishing rate-limit numbers you can tune against, secret values, or a map of unpublished endpoints. Abuse controls are described at a high level in abuse prevention and privacy design.
If you want to use the inbox rather than read about it, create a mailbox. If you need the expiry rules, read expiration troubleshooting.
