Email overview

A Worker has a request budget. Sending mail inline spends it on somebody else’s API, and when that API is slow your sign-in is slow; when it is down, your sign-in fails.

So nothing in this kit sends mail inline. Every send is a durable job: a route writes a row, a Workflow delivers it, and retries, scheduling and per-timezone sends all belong to the Workflow rather than to the request that started it. The request that asked for a magic link returns in milliseconds and does not care whether the message has left yet.

What it gives you

  • Durable delivery. A send is a row in your own D1, picked up by an every-minute scheduler, retried on its own budget. A Worker restart loses nothing.
  • Twelve templates, themed. Magic link, one-time code, welcome, security alert, invite, support reply, tester nudge, password changed, operational notice, newsletter, lead capture, marketing campaign. Four off-the-shelf themes to start from, and a customTheme object when you want your own colors and logo.
  • Tracking that is yours. Opens, clicks and unsubscribes come back through signed callbacks on your own origin, at a fixed prefix — because those URLs are minted into mail nobody can recall.
  • A suppression list that spans environments. One per project rather than one per environment: an unsubscribe in production has to stop staging too.
  • Bounce handling. Inbound mail routed to your Worker feeds the suppression list, so a hard bounce stops the next send by itself.
  • Localized sends, when i18n is composed: the shell follows the job’s recorded locale, and the payload is yours.

The transactional line is drawn by the template

A transactional message renders with no unsubscribe link and no List-Unsubscribe header, whatever context it is sent in. A marketing template forces both.

The kind is declared by the template rather than passed at the call site, so there is no context that could add an unsubscribe footer to a sign-in message, and none that could take one off a campaign.

Sending for real, from localhost

pithy dev runs the email host with real delivery on by default. A magic link you trigger from your laptop goes out through Cloudflare Email Service with the same DKIM and the same delivery logs as production.

That needs a Cloudflare login and a sending domain already onboarded. With neither, pithy dev checks what it cheaply can before spawning anything, falls back to the local simulator, says so in its ready banner with the command that fixes it, and starts the session anyway — logging the sender, recipient and subject and writing the rendered bodies to disk.

So the message is readable either way, and which one you got is said once, where you look.

What it deliberately does not do

It is not a marketing platform. There are campaign and newsletter templates and a suppression list, and there is no segment builder, no A/B testing and no drip editor. If your marketing team needs a tool, buy them one.

It does not run an inbox. Inbound mail is handled for bounces and complaints. Reading, threading and answering customer mail is support.

It does not choose your sending domain, and cannot. The from address must use a domain onboarded onto Cloudflare Email Service, and onboarding one is an account action with DNS behind it.

It does not guarantee delivery. SPF, DKIM, DMARC and reputation decide that, and Deliverability is the page about it.

The one setting to get right early

baseUrl — the public origin tracking and unsubscribe links are built against. pithy add writes the derived PUBLIC_ORIGIN constant rather than a literal, and that is the whole point: a URL typed here is one environment’s origin written into every link this capability ever mints.

The failure that prevents is specific and unpleasant: an unsubscribe from a staging test unsubscribing that person in production.

When you would reach for it

The moment you compose auth, because magic-link and one-time-code delivery needs it — auth declares it as a peer capability, so it arrives with pithy add auth --with-prerequisites whether you asked for it or not.

Separately: any message your app sends a human. A receipt, an invite, an alert, a weekly digest.

What it needs

It requires secrets: the link-signing key is read through the registry.

pithy add email writes bindings and touches no Cloudflare account. The shared suppression database and the per-environment email worker arrive with pithy email provision, which also creates the inbound bounce route when you name the zone, the address and the target worker.

ESC