Self-hosted email control plane on Cloudflare Email Routing.
Optional archive · optional forward · reply & compose as any address on your domain
Deploy a Worker in front of your catch-all. You own the routing YAML, R2 archive, SMTP, and policy.
Plain Email Routing forward is fine until it isn’t:
| Pain | What breaks | What CFEG adds |
|---|---|---|
| Best-effort forward | Dest down, full mailbox, rate limits → mail can vanish | Optional archive on receive (R2) so the domain keeps a copy |
| Identity at scale | Gmail “Send mail as” tops out at a few aliases | Reply tokens + hop so catch-all / role addresses stay speakable |
| Privacy aliases | netflix@…, github@…, that-one-marketplace@… |
One gateway path for reply & compose as the real local-part |
| Gmail UX | Custom hop headers are invisible to native Reply | CFEG Reply rewrites Reply / Reply-All |
Archive and forward are independent toggles. Run vault-only, forward-only, or both — your YAML, your call.
| Module | You get |
|---|---|
| Ingest | Catch-all → Worker; rules + default_inbox; Message-ID-aware handling |
| Archive | Optional full MIME → R2; success policy ties archive + delivery when enabled |
| Deliver | cf_forward to your inbox with X-CFEG-* v2 hop metadata |
| Reply hop | r+TOKEN@yourdomain → SMTP as the original mailbox |
| Compose / send-proxy | HTTP compose + alias+user=domain@you proxy from authorized senders |
| Observability | D1 delivery attempts; retries skip work that already succeeded |
| Gmail client | Separate MIT extension: Reply / Reply-All → hop tokens |
Sender → Cloudflare Email Routing → CFEG Worker
├─ archive? → R2
└─ forward? → inbox (+ X-CFEG headers)
Gmail (CFEG Reply) / API / send-proxy
→ CFEG hop (token)
→ SMTP as the real address on your domain
No “provision every alias in Gmail.” The gateway is the identity plane; the inbox is where you read.
- Operators already on Cloudflare Email Routing
- Catch-all privacy domains (unique local-part per service)
- Teams with many role / brand addresses on one apex
- Anyone who wants a receive log that isn’t “hope Gmail got it”
Background / narrative (blog): reyn.id — CFEG
| Piece | Role |
|---|---|
| Cloudflare Worker | This repo |
| D1 | Message + attempt metadata |
| R2 | Optional MIME archive |
| Email Routing | Catch-all → Worker name cf-email-gateway |
| SMTP (e.g. your ESP) | Compose, reply hop, send-proxy (DATA, 1:1 bodies) |
| Secrets | ROUTING_YAML, SMTP_*, optional COMPOSE_API_TOKEN |
Config is YAML you control (example in-repo for copy-paste; live value must be Worker secret ROUTING_YAML — no silent example fallback). Synthetic fixtures only in git (example.com / me@gmail.com).
Full install: docs/18-installation.md
git clone https://github.com/reynhartono/cf-email-gateway.git
cd cf-email-gateway
npm install && npm testProduction (short path):
npx wrangler login(orCLOUDFLARE_API_TOKEN)- Create D1 + R2 →
cp wrangler.toml.example wrangler.toml(bindDB/ARCHIVE) npx wrangler d1 migrations apply cf-email-gateway --remote- Copy
config/routing.example.yaml→ local file →
npx wrangler secret put ROUTING_YAML < config/routing.local.yaml - Secrets:
SMTP_HOST,SMTP_USERNAME,SMTP_PASSWORD(+ optionalSMTP_PORT,COMPOSE_API_TOKEN) npx wrangler deploy- Email Routing catch-all → Worker
cf-email-gateway - Smoke:
GET /healthmust show"routing_ok":true, then authenticated smtp-selftest / compose, then a real inbound
Secret changes require a redeploy. Missing ROUTING_YAML fails closed (email error / HTTP 503); /health stays up with routing_ok:false.
curl -sS "https://cf-email-gateway.<account>.workers.dev/health"
# {"ok":true,"service":"cf-email-gateway","routing_ok":true}
curl -sS -H "Authorization: Bearer $COMPOSE_API_TOKEN" \
"https://cf-email-gateway.<account>.workers.dev/v1/smtp-selftest?to=me@gmail.com"Gmail Reply UX: install CFEG Reply (Chrome / Firefox).
| Doc | Purpose |
|---|---|
| docs/18-installation.md | Install / deploy |
| docs/09-decisions.md | Product defaults (SoT) |
| docs/15-x-cfeg-header-contract.md | X-CFEG v2 producer contract |
| docs/02-config-and-routing.md | Routing YAML |
| docs/13-delivery-and-reply.md | Forward + hop |
| docs/17-send-proxy.md | Send-proxy addressing |
| docs/16-outbound-smtp-vs-api.md | SMTP DATA |
| docs/README.md | Full index |



