build-cloudflare-email-sending
Use if sending email via Cloudflare Email Service, replacing Resend/SES/Postmark or send_email.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use if sending email via Cloudflare Email Service, replacing Resend/SES/Postmark or send_email.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use skill if you are exhaustively testing or release-gating martool CLI commands in a source checkout or deployed Coolify container over SSH, without local Docker or provider spend.
Use if driving agent-browser for Chrome/CDP automation, @ref snapshots, tabs, or verification.
Use if testing or debugging an iOS app via agent-device CLI — simulator flows, evidence, bug triage.
Use if supervising Jean agents through MCP and Computer Use for monitoring, recovery, or closure.
Use if auditing or designing a CLI for agent/LLM use — JSON output, exit codes, non-interactive.
Use if auditing or designing an MCP server for agent-readiness — framework, security, context.
| name | build-cloudflare-email-sending |
| description | Use if sending email via Cloudflare Email Service, replacing Resend/SES/Postmark or send_email. |
Cloudflare Email Service (open beta since 2026-04) sends transactional email
through three equivalent paths — a Workers send_email binding, a REST API,
and SMTP — all sharing one pipeline, one quota, and automatic DKIM/ARC
signing. This skill gets a sending domain onboarded, picks the right send
path for the caller's architecture, and executes migrations from other
providers without breaking email mid-cutover.
Use this skill if you are:
send_email Workers binding, wrangler email sending, or a
Worker relay for a backend that runs outside Cloudflare (Railway, Fly,
Vercel, a VPS).cf-bounce DNS, DKIM/SPF conflicts, or Email Service quota
errors.This is the sending product (open beta, 2026) — not Email Routing/receiving, not the retired MailChannels path.
from when the zone moves).send_email binding
ambiently, the backend calls it over HTTPS with a shared random secret.
No Cloudflare credential ever touches the backend.wrangler --version # need >= 4.110; older versions 404 on sending endpoints
wrangler whoami # confirms login; note available account IDs
Multi-account logins fail non-interactively — export
CLOUDFLARE_ACCOUNT_ID=<id> before every wrangler email command. Find the
account that owns the target zone by probing:
CLOUDFLARE_ACCOUNT_ID=<id> wrangler email sending settings <domain>
# "Could not find a zone" → wrong account; repeat with the next id
dig +short NS <domain> # *.ns.cloudflare.com → zone is on Cloudflare; else see fact 1
CLOUDFLARE_ACCOUNT_ID=<id> wrangler email sending enable <domain>
CLOUDFLARE_ACCOUNT_ID=<id> wrangler email sending dns get <domain> # inspect what was created
enable auto-creates, on the cf-bounce subdomain: three bounce MX records,
an SPF TXT, a DKIM key at cf-bounce._domainkey, plus _dmarc.<domain>.
Verify propagation before the first send:
dig +short MX cf-bounce.<domain>
dig +short TXT cf-bounce._domainkey.<domain> | head -c 40
dig +short TXT _dmarc.<domain>
SPF conflict rule: if the root domain already has an SPF record from
another sender, Cloudflare's include:_spf.mx.cloudflare.net must be merged
into that one record — a second SPF TXT is invalid and tanks deliverability.
The cf-bounce records are namespaced and never conflict. Subdomains
(notifications.<domain>) onboard independently — use one per traffic class
for reputation isolation.
| Caller | Path | Why |
|---|---|---|
| Code already on Workers | send_email binding | Zero credentials, zero HTTP hop |
| Backend outside Cloudflare (Railway/Fly/VPS/Vercel functions) | Worker relay (this skill's template) | Token creation is dashboard-gated; a shared secret to your own Worker is the only credential |
| Legacy app that only speaks SMTP | smtp.mx.cloudflare.net:465, user api_token, password = a token with Email Sending: Edit | No code changes |
| One-off / CI / smoke test | wrangler email sending send --from ... --to ... --subject ... --text ... | Fastest proof |
For the Worker relay: copy assets/email-sender-worker/ into the repo (e.g.
workers/email-sender/), deploy, set the secret, verify:
cd workers/email-sender
CLOUDFLARE_ACCOUNT_ID=<id> npx wrangler deploy
openssl rand -hex 32 | CLOUDFLARE_ACCOUNT_ID=<id> npx wrangler secret put AUTH_TOKEN
# keep the generated value ONLY in the backend's secret store (env var / vault) — never in git
The #1 API landmine: the Workers binding's address-object shape is
{email, name} while the REST API's is {address, name}. The bundled
Worker template translates; hand-rolled code that passes {address} to the
binding fails with "Incorrect type for the 'email' field". The wrangler CLI
smoke-send accepts display strings ("Name <addr@dom>").
Never hard-cut from the old provider. Route by config: prefer Cloudflare when its env vars are set, fall back to the old provider otherwise — either provider alone keeps email alive, and rollback is unsetting two vars. Keep the old provider's fallback for 2–4 weeks of observed deliverability (the product is beta; no public inbox-placement history exists yet).
Provider-specific mappings, DNS specifics, and code-shape diffs:
references/migrate-from-resend.mdreferences/supabase-auth-emails.mdreferences/greenfield-architecture.mdEvery leg gets a live probe — a green deploy is not a delivered email:
# 1 Worker auth gate: wrong token must 401, missing config must 503
curl -s -X POST https://<worker>/send -H "Authorization: Bearer wrong" -d '{}' -o /dev/null -w "%{http_code}\n"
# 2 Real send returns a message_id
curl -s -X POST https://<worker>/send -H "Authorization: Bearer $EMAIL_SENDER_TOKEN" \
-H "content-type: application/json" \
-d '{"from":{"address":"welcome@<domain>","name":"Product"},"to":"<real inbox you control>","subject":"cutover probe","text":"probe"}'
# 3 The email actually lands in that inbox (check spam folder too)
# 4 The OLD provider's send log shows nothing new → traffic really moved
# 5 Quota headroom: GET /accounts/<id>/email/sending/limits (or the dashboard)
Delete any test users/rows the probes created. If probes sent to fake
addresses, expect them in permanent_bounces — repeated hard bounces
auto-suppress recipients, and unsuppression requires a Cloudflare support
case, so always probe with a real inbox you control.
AUTH_TOKEN lives in exactly two places: wrangler secret and
the backend's secret store. Never in git, wrangler.jsonc, docs, or chat.wrangler secret put AUTH_TOKEN + update
the backend var in the same change window.openssl rand -hex 32 as the
generation step, never a literal value.| Symptom | Cause | Fix |
|---|---|---|
wrangler email sending → 404 | wrangler < 4.110 | npm i -g wrangler@latest |
| "More than one account available" | multi-account login | CLOUDFLARE_ACCOUNT_ID=<id> |
| "Incorrect type for the 'email' field" | REST shape sent to the binding | {email} for binding, {address} for REST |
E_SENDER_NOT_VERIFIED | domain/subdomain not onboarded | wrangler email sending enable + DNS check |
429 / E_RATE_LIMIT_EXCEEDED | daily quota (default ~1,000) | limit-increase form; queue + retry with backoff |
| Lands in spam | DMARC alignment missing | verify _dmarc TXT exists; merged SPF on root |
| Recipient silently gets nothing | auto-suppressed after hard bounces | Cloudflare support case; stop probing fake addresses |
| Dashboard shows sends as "dropped" | known Email Routing metrics false-negative | trust the send response's message_id, not that panel |