Skip to content

Mailbox platform (consumer patterns)

How to build a shared mailbox / email product on top of Tamer-managed Cloudflare Email Service. Tamer owns env namespaces, Routing/Sending domains, catch-all, and deploy; your Worker owns addressing, storage, and product policy.

This is the pattern used by mailbox apps (e.g. HQBase-style / AAT): one ingress, app-level aliases, unrestricted send with policy in code.

What Tamer owns vs what you own

LayerOwner
post.{env|ephemeral}.… / notify.… hostnamesTamer email config
Catch-all → durable dump / ingress WorkerTamer routing.catchAll (resolveEnv)
Optional prefixed ephemeral rulesTamer email.rules
Wrangler send_email bindingTamer resources.emailSend
Alias → mailbox resolutionYour app (D1 / DO / etc.)
Ephemeral env parse + DISPATCHYour app
Unknown recipient policyYour app
MIME parse, raw store, dedupeYour app
From/to allow policy for open sendYour app

Do not create a Cloudflare Routing rule per mailbox alias. That fights ephemeral envs and turns every user signup into an IaC apply.

Mirror WFP: one dump hostname, one durable dump Worker, env in the address / tenant script name — not a new CF rule or ingress Worker per PR.

Internet mail
    → MX on post.{env|ephemeral}.…  (e.g. caa.post.ephemeral.…)
    → Email Routing (literal rules + zone catch-all)
    → durable account Worker `email_ingress` (`email()` handler)
    → if hostname is *.ephemeral.*: parse ^(pr-\d+)- from local-part
         → DISPATCHER.get("api-{ws}-{env}")
    → else: resolve mailbox in your DB / prod policy

Cloudflare’s catch-all is zone-wide (there is no *@hostname matcher). Enable the ephemeral routing hostname; unmatched addresses such as pr-42-jane@caa.post.ephemeral.… hit routing.catchAll.

1. Catch-all → one durable dump Worker

Pin catch-all with resolveEnv (never CLI --env). Deploy that Worker from the pinned env; bind it to the ephemeral dispatch namespace (and prod if the same Worker also handles long-lived hosts).

ts
email: {
  zoneId: "…",
  routing: {
    domains: [/* post.prod / post.ephemeral — see Email Service guide */],
    catchAll: {
      resolveEnv: "prod",
      action: { type: "worker", worker: "email_ingress" },
    },
  },
  // Prefer no per-PR rules — addresses fall through to catch-all.
}

Optional: a durable dump rule on an ephemeral hostname without ${tamer:env} needs both preserveOnDestroy and resolveEnv (same pin idea). Still prefer catch-all for true *@….ephemeral.… delivery.

2. App-level mailbox + ephemeral routing

In email(message, env):

  1. Normalize message.to (envelope recipient).
  2. If hostname matches *.ephemeral.* (or your dump host):
    • Parse env from the local-part (^(pr-\d+)- / your pattern).
    • env.DISPATCHER.get(\api-${workspace}-${env}`)` (or your RPC) into the tenant script in the ephemeral dispatch NS.
  3. Else look up mailbox / workspace by address in D1 (or equivalent).
  4. Known → deliver to that mailbox’s inbox.
  5. Unknown → product catch-all policy (folder, reject, or admin mailbox). Store the policy on the domain row in your DB — not as CF rule churn.

Keep today’s address shape: pr-42-jane@caa.post.ephemeral.….

3. Unrestricted emailSend for mailbox products

ts
resources: {
  emailSend: [
    { logicalName: "mail", binding: "MAIL_SENDER", remote: true },
  ],
}

No allowed_* fields → Wrangler { name: "MAIL_SENDER" }. Enforce sender domains / recipient policy in application code. Use allowlisted bindings for transactional-only Workers (password reset, billing) — see Email Service.

4. Parse once; store raw + structured

  • Persist the raw message (R2 / object store) for audit and reprocess.
  • Persist a parsed projection (headers, text/html, attachment metadata) for UI and search.
  • Dedupe on (Message-ID, envelope-recipient) so CF retries do not double-file.

5. Domain catch-all policy in the app DB

Per custom domain / workspace: reject | store | route_to_mailbox. Evaluate after CF has already delivered to your ingress — CF catch-all is delivery plumbing; product catch-all is UX.

6. DISPATCH behind ingress (required for WFP tenants)

Email Routing cannot target Workers for Platforms dispatch scripts. Keep an account dump / ingress Worker, then env.DISPATCH.get(scriptName).fetch(…) (or your own RPC) into the tenant Worker. Tamer rejects dispatchNamespace on routing actions for this reason.

7. Onboarding / readiness UX

If you ship a “connect domain” wizard, mirror Tamer’s status inspect:

  1. Routing enabled + DNS ready
  2. Catch-all → your dump / ingress script
  3. Sending subdomain enabled for the brand hostname

tamer status already prints this bundle for operators. Product UIs can call the same Cloudflare APIs (or a thin admin Worker) with the same step list and resumable errors (Workers Paid, Zone Settings Edit).

Env matrix (reuse Tamer namespaces)

EnvInbound hostIsolation
prod / staging / devpost.{env}.…Host + ingress script
pr-* (ephemeral)post.ephemeral.… (shared)Local-part env prefix + durable dump Worker → DISPATCH

Tenant aliases like user@acme.post.prod.… stay in your DB. Only use ${tamer:env}-…@ CF literal rules when you intentionally want a per-env ingress Worker (Path C-alt) — not for per-user mailbox addresses.

See also

Released under the Tamer Evaluation License.