Add Email

pithy add email --with-prerequisites

--with-prerequisites composes secrets, which email requires — the link-signing key is read through the registry.

You may never type this command. auth declares email as a peer capability, so pithy add auth --with-prerequisites composes it for you, deepest first.

What lands in your repo

apps/<worker>/pithy.config.ts gains the registration:

email({
  fromAddress: "noreply@example.com",
  fromName: "Pithy",
  baseUrl: PUBLIC_ORIGIN,
  theme: "saffron",
}),

fromAddress and fromName are the two to change first. The address must use a domain onboarded onto Cloudflare Email Service, and the name is what recipients see — a recognizable one improves deliverability.

baseUrl is written as the derived PUBLIC_ORIGIN constant rather than a literal, because a URL typed there is one environment’s origin written into every link this capability mints.

apps/<worker>/wrangler.jsonc gains three bindings, in every environment stanza:

BindingTypeWhat it is
DBd1Where the job rows live — the same app database
EMAIL_SUPPRESSIONSd1The project-wide opt-out list, shared across environments
EMAIL_SENDERworkflowThe Workflow that delivers a queued message

The Workflow entry names the capability’s host Worker across scripts, so it is complete before that host exists and wrangler dev binds it either way. The host itself is deployed by provision.

What runs, and what is minted

add runs that Worker’s dev migrations, and mints email-link-signing-key into <config>/<project>/secrets.jsonc — outside every checkout. Deployed environments get their own from pithy secrets provision, because a link-signing key is arbitrary: any random string works, since nothing outside the project validates one.

The seven config options

OptionDefaultWhat it decides
fromAddressnoreply@example.comThe address every message is sent from. Must use an onboarded domain
fromNamePithyThe sender display name recipients see
baseUrlPUBLIC_ORIGINThe public origin tracking and unsubscribe links are built against, so it must be reachable from a recipient’s inbox
basePath/emailWhere the management routes mount — the send log and the suppression list. Every one is control-plane and default-denied
themesaffronOne of four off-the-shelf themes to bootstrap from. Fine-tune with a customTheme object
schedulerEnabledtrueWhether the every-minute scheduler Workflow runs, finding due scheduled and per-timezone sends
devDeliveryremoteWhat the prebuilt worker does under pithy dev. remote sends real mail; simulator logs and writes to disk instead

The recipient callbacks — click, open, unsubscribe — keep a fixed prefix and are unaffected by basePath, because those URLs are already minted into mail nobody can recall.

Then provision

pithy email provision

That creates the project’s shared suppression database, migrates it, and deploys the per-environment email worker.

Add the inbound bounce route when the subdomain exists:

pithy email provision \
  --routing-zone <zone-id> \
  --inbound-address bounce@bounce.example.com \
  --app-worker acme-prod

The three routing flags are all or nothing, and the reason is worth reading before you type them: enabling Email Routing on a zone points its MX at Cloudflare. Use a subdomain, never your apex, or you will move your real inbound mail off your existing provider.

See a template before you ship it

pithy email test --to you@example.com --template magicLink

That renders one template through your project’s own configuration — identity, theme and all — and sends it. It deploys nothing, and tracking is off for that message.

Check it worked

pithy doctor reports email under the Worker’s health, and its settings check is one of the two tiers that actually looks at a value: a fromAddress on a domain nobody onboarded is a finding rather than a silent failure on the first send.

ESC