A self-hosted disposable email service that runs entirely on Cloudflare Workers — no VPS required. It receives inbound mail through Cloudflare Email Workers, stores messages in D1, and serves a clean web UI from the edge.
- How it works
- Prerequisites
- Setup Guide
- Commands cheat sheet
- Project structure
- Tech stack
- Abuse controls & retention
- Tests
- Troubleshooting
- Documentation
- License
Sender → Cloudflare MX → Email Worker (email handler)
│
▼
D1 Database (SQLite)
│
▼
Worker HTTP handler → Web UI + API
- No VPS — everything runs on Cloudflare's edge
- No Postfix — Cloudflare Email Workers handle SMTP ingestion natively
- No Docker — just
wrangler deploy - Zero cost — fits within Cloudflare's free tier
- Installable PWA — add to home screen, offline app shell
Before you start, you need:
| Requirement | Details |
|---|---|
| Cloudflare account | Sign up here (free) |
| A domain | Must be added to Cloudflare (nameservers pointed to Cloudflare) |
| Node.js | v18 or later (download) |
| npm | Comes with Node.js |
git clone <your-repo-url>
cd Disposable-Temp-Mail
npm installnpx wrangler loginThis opens a browser window. Log in with your Cloudflare account and approve the OAuth scopes.
What scopes are needed? Wrangler will request permissions for Workers, D1, Email Routing, Pages, and more. You must approve all of them so the CLI can create the database and deploy the worker.
Verify you're logged in:
npx wrangler whoamiCopy the example config and fill in your own values:
cp wrangler.example.toml wrangler.tomlThen edit wrangler.toml:
| Field | Change to |
|---|---|
database_id |
Leave empty for now — you'll fill it in Step 4 |
routes pattern |
Your web UI subdomain (e.g. tmail.example.com) |
MAIL_DOMAIN |
Your receiving domain (e.g. example.com) |
WEB_HOST |
Same as the routes pattern (e.g. tmail.example.com) |
wrangler.tomlholds your private domains and database ID — it is git-ignored. Onlywrangler.example.toml(placeholders) is committed.
npx wrangler d1 create disposable-temp-mail-dbYou'll see output like:
✅ Successfully created DB 'disposable-temp-mail-db'
[[d1_databases]]
binding = "DB"
database_name = "disposable-temp-mail-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Copy the database_id into your wrangler.toml.
Push the schema to your remote D1 database on Cloudflare:
npx wrangler d1 execute disposable-temp-mail-db --remote --file=src/db/schema.sqlThis creates six tables:
inboxes— email addresses (with per-inboxretention_days)messages— received emailssessions— browser session tokenssession_inboxes— which inboxes belong to which sessioninbox_tokens— cross-device transfer codesrate_hits— rate-limit counters (pruned daily)
Existing deployments: re-run the command after pulling updates — the schema uses
CREATE TABLE IF NOT EXISTS, so it safely adds missing tables.
Note: The
--remoteflag is important — without it, the schema only applies locally. You want it on Cloudflare's servers.
npx wrangler deployThis does three things:
- Uploads the TypeScript Worker code
- Uploads the static frontend files (HTML/CSS/JS) to Cloudflare Assets (edge CDN)
- Registers the custom domain route
After a successful deploy, you'll see:
Deployed disposable-temp-mail triggers
tmail.YOURDOMAIN.com (custom domain)
Cloudflare automatically creates the DNS record for your Worker's custom domain. If it doesn't:
- Go to Cloudflare Dashboard → Workers & Pages → disposable-temp-mail → Settings → Domains
- The custom domain
tmail.YOURDOMAIN.comshould already be listed
Email Routing should already be enabled on your domain. Verify:
npx wrangler email routing settings YOURDOMAIN.comIt should show Enabled: true. The catch-all rule is also automatically set up — every *@YOURDOMAIN.com is routed to the disposable-temp-mail Worker:
npx wrangler email routing rules list YOURDOMAIN.comExpected output:
Catch-all rule: enabled, action: worker:disposable-temp-mail
If you don't already have an SPF record, add one so emails don't get flagged as spam:
| Type | Name | Content |
|---|---|---|
| TXT | @ |
v=spf1 include:_spf.mx.cloudflare.net ~all |
- Open
https://tmail.YOURDOMAIN.comin your browser - Click New → Create to file a random address (or type a name first for a custom one)
- Send an email from Gmail/any provider to that address
- Press R (or the refresh control) — the email appears in the register
- Expand the entry to read it; Strike from ledger permanently deletes a single message
| Command | What it does |
|---|---|
npm run deploy |
Deploy Worker + static assets |
npm run db:migrate |
Apply schema to production D1 |
npm run db:local |
Apply schema to local D1 (for dev) |
npx wrangler dev |
Run Worker locally |
npx wrangler tail |
Stream live logs from production |
npx wrangler d1 execute disposable-temp-mail-db --remote --command="SELECT * FROM messages LIMIT 10" |
Query the database |
npx wrangler d1 execute disposable-temp-mail-db --remote --command="SELECT * FROM messages ORDER BY received_at DESC LIMIT 5;"npx wrangler tail --format prettyThen send a test email — you'll see the Worker processing it in real time.
disposable-temp-mail/
├── wrangler.example.toml # Template config (copy to wrangler.toml, git-ignored)
├── package.json
├── tsconfig.json
├── .gitignore
├── docs/ # Dedicated documentation & visual guides
│ ├── API.md # Complete REST API reference
│ ├── SECURITY.md # Security policy and scope notes
│ ├── CHANGELOG.md # Project changelog
│ └── assets/ # Blotcat architectural illustrations
└── src/
├── index.ts # Entry point: fetch() + email() + scheduled() handlers
├── cleanup.ts # Daily retention purge (messages, inboxes, sessions)
├── email-handler.ts # Parses inbound email via PostalMime → D1
├── api/
│ └── routes.ts # Hono router: /api/config, /api/session, /api/inboxes, /api/messages
├── db/
│ ├── schema.sql # D1 tables (inboxes, messages, sessions, session_inboxes, rate_hits)
│ └── queries.ts # Typed query functions
├── utils/
│ ├── random-address.ts # Human-like random email generator
│ └── rate-limit.ts # D1-backed sliding-window rate limiter
└── web/
├── index.html # Frontend UI
├── app.js # Frontend logic (vanilla JS)
└── styles.css # Dark theme styles
| Layer | Tech |
|---|---|
| Runtime | Cloudflare Workers |
| Router | Hono |
| Email parsing | PostalMime |
| Database | Cloudflare D1 (SQLite) |
| Static hosting | Cloudflare Workers Assets (edge CDN) |
| Language | TypeScript |
| CLI | Wrangler v4 |
- Rate limits (per hour, tunable in
wrangler.toml): 20 inbox creations per session, 30 inbox creations per IP, 10 new sessions per IP, 30 transfer-code claims per IP. Exceeded requests get429withRetry-AfterandX-RateLimit-*headers. - Bot gate (recommended for public instances): inbox creation can require a
Cloudflare Turnstile
token. Create a widget for your web host (Managed mode), then:
The gate stays dormant until both are set.
npx wrangler vars set TURNSTILE_SITE_KEY # Site Key (public) npx wrangler secret put TURNSTILE_SECRET_KEY # Secret Key (never commit this) npx wrangler deploy
- Retention: each inbox carries its own keep-for plan — 3, 7, 30, 90, or
180 days, or keep-until-removed — chosen at creation and changeable later (the reader
also offers Renew to restart the clock). A daily cron (
0 3 * * *) deletes inboxes past their plan together with their remaining messages, every message older than 90 days whatever the plan, sessions older than 30 days, and stale rate-limit rows. Individual messages can also be struck from the ledger anytime — permanent and immediate, no waiting for the purge.
Zero-dependency suite on Node's built-in runner with an in-memory SQLite
D1 shim (src/test-helpers.ts) — covers address generation, rate limiting,
queries, retention purge, and email parsing:
npm testYour domain's nameservers are not pointed to Cloudflare, or the DNS record hasn't propagated yet. Check:
dig +short YOURDOMAIN.com NSShould show *.ns.cloudflare.com. Propagation can take up to 24 hours after changing nameservers.
- The email was received but the inbox hasn't been linked to your browser session. Click New → type the exact username → Create to file it (or paste its transfer code into the Link field).
- Check the database:
npx wrangler d1 execute disposable-temp-mail-db --remote --command="SELECT * FROM messages ORDER BY received_at DESC LIMIT 5;" - Check live logs:
npx wrangler tail --format pretty
This is a known wrangler warning — it's cosmetic. The [email] config works fine. Cloudflare is still stabilizing the Email Worker integration.
This project uses Wrangler v4. If you're on v3:
npm install --save-dev wrangler@4Comprehensive project documentation, security guides, and API contracts live in the docs/ directory:
- REST API Reference — All endpoints (
/api/session,/api/inboxes,/api/messages), authentication headers, rate limits, and cURL workflows. - Security Policy & Boundaries — Vulnerability reporting protocol and disposable threat model.
- Changelog — Version history and release notes.
- Architectural Illustrations — Hand-drawn 16:9 Blotcat system guides.
Apache-2.0 — see LICENSE.

