Why catch-all stops being enough
Cloudflare Email Routing has two operating modes. The catch-all forwards every *@yourdomain to a single action — usually a Worker that lands the mail in your D1. It’s the default at onboarding, and for the first few weeks of a domain’s life it’s exactly what you want: you don’t know which addresses matter yet, so you let everything through.
The trouble starts when which addresses matter becomes a question. Maybe you want support@ and press@ to land in the inbox but careers@ and webmaster@ to bounce so the long tail of spam doesn’t fill your storage. Or you want a different group of addresses per domain in the same Cloudflare account. None of that is expressible as a single catch-all rule on a zone — you need a collection of rules.
The route table, and why we call it a table
A route table is a named, ordered list of local parts — the bit before the @ in an email address. The name is deliberate: each row pairs a local part with the full local@domain address it will produce, exactly like a column in a SQL table. The list is deduped and sorted, so two routes targeting the same address collapse to one rule on Cloudflare.
Bind a table to a domain and Flowmails translates each row into one Email Routing rule: a literal to matcher on local@yourdomain, forwarding to the same Worker the catch-all would have. The catch-all is disabled at the same time — only the addresses in the table receive mail. Unbind and the literal rules are removed; the catch-all stays disabled until you explicitly restore it (we don’t auto-revert because the user’s intent at unbind time is usually “I want a fresh start,” not “please forget that I unbound”).
# A route table is a JSON array of local parts.
# Bind it to a domain to install one literal matcher per entry.
[
"support",
"sales",
"press"
]
Data flow: from support@ to your inbox
- Someone sends mail to support@yourdomain. SMTP resolves to Cloudflare’s MX; the message lands at the Email Routing layer.
- Cloudflare walks the literal rules for the zone in priority order. The first matchers: [{type:"literal", field:"to", value:"support@yourdomain"}] hit forwards to your email-worker.
- The Worker writes a row to email in D1 with the parsed envelope, then a row to email_address for the recipient, then a row to email_content for the body. Attachments go to your R2 bucket.
- Your dashboard’s Emailbox page reads from those tables via the email-worker’s REST API. The top-of-page banner shows the route table that’s bound to the active domain, so a user opening the inbox sees exactly which addresses receive mail.
System presets, hardcoded on purpose
Most teams want one of four shapes. To save everyone the trouble of re-typing the same three local parts, the Flowmails shared package ships four hardcoded presets — no database row, no migration, no seed:
Common support aliases for small businesses.
support, sales, contact
Author-facing aliases for content creators.
author, hello
Lean trio for early-stage products.
support, info, noreply
Public relations and partnership aliases.
press, media, partnerships
Picking a preset is a form prefill, not a persistent reference — Flowmails stores only the local parts on the domain’s binding row, never the template id. This means a future renaming of Standard to Standard v2 won’t retroactively change any user’s domain; they always see exactly what they wrote.
Building a custom template
The Route Templates page is a library of your own tables. Creating one starts with a name and a list of local parts; the “Copy from” dropdown lets you seed the editor with a system preset (e.g. Personal Blog) or another one of your templates, then add or remove parts to taste. The result is deduped, sorted, and saved as a row in email_routes_template with a unique (user_id, name) constraint.
Templates never own a domain — they’re just a convenient starting point. Deleting a template doesn’t touch any binding row that was filled from it, because the binding only stored the local-part array.
Bind, unbind, restore — and the warning you’ll see if you skip a step
The three operations sit on a tiny state machine:
- Bind a table to a domain: Flowmails diffs the desired local parts against the literal rules already on the zone (filtered by a userId-anchored prefix so multi-tenant zones don’t touch each other’s rules), creates missing rules, deletes removed ones, disables the catch-all on the first bind, and writes the binding row.
- Unbind: deletes every literal rule we own on the zone and removes the binding row. The catch-all is not auto-restored — your zone now has no receiving rules, which means mail will bounce until you rebind or click restore.
- Restore catch-all: re-installs the Flowmails catch-all via the same configureEmailRouting code path used at onboarding, and flips the domain’s routing_mode column back to catchall. The binding row stays in D1 as inert history.
The Emailbox banner surfaces this state machine: a domain in rules mode with zero addresses shows an amber warning telling you mail will bounce until you rebind or restore. The dashboard’s domain detail page adds explicit Unbind and Restore catch-all buttons so the recovery is one click, not a guessing game.
Try it
Route tables ship as a first-class part of every Flowmails account. The fastest path is during onboarding — after the email-worker deploys to a zone, the catch-all step now offers a picker that previews the system presets. From the dashboard, open a domain to bind, unbind, or restore. Or jump straight to the route-templates library to start composing your own tables.
If you read this far, you’ll probably enjoy the deeper runtime tour in How Workers-native email routing actually processes your inbound mail.