Email System
Last Updated
July 16, 2026
The Pool sends transactional and campaign-support email through Resend from the Cloudflare Worker. Templates live in worker/src/email.js, shared localized copy lives in _data/i18n/*.yml, and scheduling / audience selection lives mostly in worker/src/index.js.
The email system is Worker-owned so pledge state, localization, branding, magic links, campaign scope, and resend/retry behavior stay in one path.
Source Files
worker/src/email.jsbuilds and sends email payloads.worker/src/email-outbox.jsowns durable enqueue, frozen rendering, retries, Resend event verification, and suppression.worker/src/index.jscalls email helpers from checkout, pledge management, scheduler, admin, report, and Blast routes._data/i18n/en.ymland_data/i18n/es.ymlhold shared UI/runtime/email copy.assets/i18n.jsonis the generated catalog the Worker can fetch for localized email copy._config.ymlholds non-secret sender identity and email branding inputs.worker/wrangler.tomlreceives mirrored non-secret email vars.
Senders
Current sender roles:
PLEDGES_EMAIL_FROMsends pledge lifecycle email: confirmation, modification, cancellation, payment failure, and charge success.UPDATES_EMAIL_FROMsends updates and operational email: launch reminders, abandoned-checkout reminders, diary updates, milestones, Blast, admin access, campaign assignment, protected previews, and reports.SUPPORT_EMAILbecomes the default reply-to address when configured.
These values come from _config.yml:
platform:
support_email: [email protected]
pledges_email_from: "The Pool <[email protected]>"
updates_email_from: "The Pool <[email protected]>"
The sender domains must be authorized in Resend. For the live Dust Wave deployment, the sender domain is site.example.com, so [email protected] and [email protected] require that Resend domain to be verified.
Setup
1. Configure Sender Identity
Set the sender fields in _config.yml, then sync the Worker mirror:
npm run sync:worker-config
The main dev/test/deploy scripts run this sync automatically, but running it directly is useful after manual config edits.
2. Verify The Resend Domain
In Resend:
- Add the exact sending domain used by
PLEDGES_EMAIL_FROMandUPDATES_EMAIL_FROM. - Add the DNS records Resend provides.
- Wait for verification.
- Confirm the API key is allowed to send from that domain.
Authorizing example.com does not automatically authorize pool.example.com, and authorizing pool.example.com does not automatically authorize example.com.
3. Store The API Key
Local development:
npm run secrets:dev
Production:
wrangler secret put RESEND_API_KEY
Do not store RESEND_API_KEY in _config.yml, campaign front matter, dashboard drafts, or committed docs.
4. Configure Branding
Email branding uses a curated mirror of platform.* and design.*:
EMAIL_LOGO_PATHEMAIL_FONT_FAMILYEMAIL_HEADING_FONT_FAMILYEMAIL_COLOR_TEXTEMAIL_COLOR_MUTEDEMAIL_COLOR_SURFACEEMAIL_COLOR_BORDEREMAIL_COLOR_PRIMARYEMAIL_BUTTON_RADIUS
When SITE_BASE is localhost, embedded email images fall back to the public site asset base so inbox clients do not receive broken localhost image URLs.
5. Configure Delivery Webhooks
Production delivery does not require Resend Contacts or Broadcasts. Pool remains the audience and consent source of truth and sends one /emails request per recipient.
Create a Resend webhook endpoint for:
https://worker.example.com/webhooks/resend
Subscribe to delivered, bounced, complained, failed, and suppressed email events, then store the returned Svix signing secret as RESEND_WEBHOOK_SECRET. The Worker verifies the raw body, svix-id, timestamp, and signature before updating delivery state. Open/click tracking is not used for payment or audience truth.
Email Types
Admin Magic Link
Sent when an admin requests sign-in from /admin/ or /es/admin/.
- Uses a short-lived login nonce.
- May require Cloudflare Turnstile before the email is sent.
- Local development can expose the login URL in the browser only for localhost/test paths.
- Sender values are normalized to avoid email-header injection.
Admin User Created
Sent when a super admin creates a dashboard user and notification delivery is enabled for that action.
- Explains what access was added.
- Points the user to the dashboard sign-in page.
- Does not include a password.
- Uses Resend only when
RESEND_API_KEYis configured.
Campaign Assignment
Sent when campaign users are assigned during new campaign creation or related admin flows.
- Identifies the campaign access.
- Points to the admin dashboard.
- Uses the shared admin email path.
Protected Campaign Preview
Sent when an authorized dashboard user invites reviewers to a protected campaign preview.
- Reviewer links are signed and expire after 24 hours.
- Preview access email addresses live only in short-lived Worker KV allowlists.
- Previewer emails are not committed to campaign Markdown, public JSON, sitemap output, or generated metadata.
Pledge Confirmation
Sent after a setup-mode Stripe checkout session is confirmed and the Worker persists the pledge.
Includes:
- campaign title and pledge items
- subtotal, tip, tax, shipping, and total where applicable
- Manage Pledge magic link
- supporter community link when the campaign has decisions
- localized copy based on stored
preferredLang
For bundled multi-campaign checkout, the Worker persists campaign-scoped pledges and sends campaign-specific supporter email.
Pledge Modified
Sent when a supporter changes an active pledge.
Includes:
- previous and new pledge context
- cent-accurate deltas
- updated items
- Manage Pledge link
Same-price structural changes, such as add-on variant swaps, still count as real changes and can send an update email.
Pledge Cancelled
Sent after an active pledge is cancelled before deadline.
Includes:
- confirmation that the saved card was not charged
- final pledge breakdown
- campaign link for re-pledging if the campaign remains live
Cancellation also removes the supporter from future campaign-update audiences when no active pledge remains for that email/campaign.
Payment Failed
Sent when an off-session settlement PaymentIntent fails.
Includes:
- amount due
- campaign and pledge details
- Update Card call to action through Manage Pledge
Payment method updates remain available after the campaign deadline so supporters can recover failed cards.
Charge Success
Sent when settlement successfully charges a pledge.
Includes:
- final amount charged
- campaign/tier breakdown
- tax, shipping, and tip where applicable
Diary Update
Sent when new campaign diary entries are broadcast.
- Uses a plain-text excerpt generated from the diary content.
- Links back to the campaign diary.
- Tracks sent entries in KV so the same diary entry is not broadcast repeatedly.
- Updating existing diary metadata should not send a new email when the entry ID is stable.
Milestone
Sent when a pledge pushes a campaign across configured milestone thresholds.
- Triggered from pledge persistence.
- Uses the shared Resend path.
- Keeps milestone copy short and campaign-scoped.
Blast / Announcement
Sent from Campaigns -> Blast or the legacy announcement endpoint.
Current behavior:
- Campaign users can send only for assigned campaigns.
- Super admins can send for any campaign.
- Dry runs validate subject, content, CTA, and indexed audience before sending.
- Test sends go only to the signed-in admin.
- Live sends require a matching dry-run hash.
- Live sends write an admin audit event after dispatch.
- Sent history reads recent audit records.
Blast content supports email-safe WYSIWYG blocks:
- headings
- text
- quotes
- lists
- links
- campaign-hosted images from
/assets/images/... - existing campaign images selected from the media picker
- YouTube/Vimeo links rendered as email-safe links/buttons
Arbitrary remote image hotlinks, iframes, and embedded video players are omitted from email payloads.
Launch Reminder
Sent once when an upcoming campaign becomes live, only to people who explicitly signed up.
Current behavior:
- Public signup forms can use Cloudflare Turnstile.
- Signups dedupe by campaign/email hash.
- Suppression markers are checked before send.
- Sent markers prevent duplicate delivery.
- Unsubscribe links are signed and campaign-scoped.
- The dispatch queue uses a queue-state marker so idle cron ticks skip namespace scans.
Abandoned Checkout Reminder
Sent only when the supporter explicitly opts into one checkout reminder.
Current behavior:
- The Worker queues the reminder only after Stripe successfully creates a Checkout Session.
- Successful pledge persistence deletes the pending reminder for that order.
- Before sending, the Worker checks campaign pledge indexes to avoid emailing supporters who completed through another order.
- Reminder links use signed unsubscribe and resume tokens.
- Resume links restore a sanitized checkout/cart snapshot and start a fresh Stripe session.
- The snapshot never stores Stripe secrets in the URL.
ABANDONED_CART_TOKEN_SECRET is preferred for reminder signing and falls back to MAGIC_LINK_SECRET when omitted.
Campaign Runner Reports
Sent on the configured schedule to campaign runner_report_emails.
Report types:
- daily campaign pledge ledger during live campaigns
- one-time post-deadline fulfillment export
Current behavior:
- Timing uses
platform.timezone. - CSV attachments are optional by config.
- Campaign-runner recipients receive campaign-fulfilled rows.
platform.support_emailcan receive separate platform-fulfillment rows when platform add-ons need fulfillment.- Dashboard Reports previews/downloads are read-only and do not send email or write sent markers.
Delivery And Retry
Production sends use email-outbox:v1:* records. Persistence happens before the Resend side effect, and the minute scheduler drains bounded batches. Each job:
- has a deterministic Pool job ID and Resend
Idempotency-Key - renders once, freezes the exact provider payload, and records a content hash
- uses a processing lease so crashed work can resume
- respects
Retry-After, quota responses, and bounded exponential delay - refuses blind retry after an ambiguous response outlives Resend’s 24-hour idempotency window
- expires transient payload data after 30 days and retains only minimal
email-delivery:v1:*evidence for 400 days
sendPreparedResendEmail is the central provider helper. It:
- posts to
https://api.resend.com/emails - uses
Authorization: Bearer ${RESEND_API_KEY} - includes HTML and plain-text bodies
- applies reply-to when
SUPPORT_EMAILis configured - summarizes Resend provider errors
- returns normalized retryability, ambiguity, status, and provider error type on non-OK responses
The older supporter-email-retry:* queue remains readable during migration, but retries now hand final delivery to the shared outbox. Admin sign-in magic links and explicit test sends remain immediate because delaying them would make the interaction unusable.
Diary, milestone, and live announcement mail carries a signed campaign-scoped List-Unsubscribe URL and RFC 8058 List-Unsubscribe-Post header. GET shows a human confirmation; POST returns a blank success and writes an indefinite hashed campaign suppression. Transactional pledge/payment mail is not suppressed by campaign marketing preferences.
Permanent bounces, complaints, and provider suppressions create a hashed local email-suppression:v1:* marker. The outbox checks both global and campaign suppression immediately before provider delivery.
Consent And Trust
Email changes should follow the Ethical Risk review when they add new audiences, triggers, reminders, reports, or marketing surfaces.
Rules:
- Send supporter communications only from an explicit pledge, explicit signup, admin-authorized campaign scope, or documented operational need.
- Keep launch reminders and abandoned-checkout reminders opt-in, bounded, deduped, and suppressible.
- Run dry-run or no-send evidence before new bulk-send paths.
- Do not use urgency, scarcity, or personalization in a way that misrepresents campaign state, inventory, deadlines, tax, shipping, fees, or pledge totals.
- Keep plain-text bodies, localized links, sender identity, and support/reply-to behavior understandable without requiring the recipient to inspect HTML.
- Treat Blast, diary, milestone, preview, and assignment emails as trust-sensitive surfaces; review who receives them, what data they reveal, and how a mistaken send can be contained.
Copy And Localization
Human-facing copy should stay short, direct, and localizable.
Rules:
- Put shared email copy in
_data/i18n/en.ymland_data/i18n/es.yml. - Let
worker/src/email.jskeep fallback English strings for missing keys. - Preserve
preferredLangfrom checkout/signup flows where available. - Use localized
/manage/and/community/:slug/links when the locale route exists. - Avoid adding long campaign-authored copy to translation YAML; campaign content belongs in campaign files.
Content Safety
Email content has stricter constraints than campaign-page content:
- Strip control characters from configurable header values.
- Escape campaign/user-controlled text before rendering HTML.
- Generate plain text from HTML for clients that need it.
- Use only site-hosted images in Blast emails.
- Convert YouTube and Vimeo content to links/buttons, not embeds.
- Avoid SVG attachments and active-content attachment patterns.
- Keep tokenized links scoped, signed, and time-limited where possible.
Testing
Focused unit coverage:
npm run test:unit -- \
tests/unit/email-tip.test.ts \
tests/unit/email-security.test.ts \
tests/unit/email-broadcasts.test.ts \
tests/unit/email-outbox.test.ts \
tests/unit/worker-ops-integrity.test.ts
Useful broader checks:
npm run test:unit
npm run test:i18n
npm run test:secrets
npm run release:payment-smoke -- --no-dev-vars
For release evidence that should render email payloads without calling Resend, set POOL_EMAIL_DRY_RUN=true or RESEND_EMAIL_DRY_RUN=true. The shared send path returns a dry-run id and skips the provider request, which lets pledge, report, launch-reminder, abandoned-checkout, and Blast-adjacent smoke checks prove payload construction without sending mail.
Manual smoke:
- Start the Podman stack with
./scripts/dev.sh --podman. - Complete a test checkout.
- Confirm pledge persistence and supporter confirmation behavior.
- Trigger a pledge modification and cancellation on a local test campaign.
- Test Campaigns -> Blast dry-run and test-send from the dashboard.
- Preview/download Reports and confirm no email is sent by dashboard downloads.
- Review Resend dashboard errors if delivery fails.
Troubleshooting
If email does not send:
- Confirm
RESEND_API_KEYis set in the Worker runtime. - Confirm the sender domain matches
PLEDGES_EMAIL_FROM/UPDATES_EMAIL_FROM. - Check Resend domain verification status.
- Check Worker logs for the summarized provider error.
- Confirm local development is not relying on a production-only secret.
- Run
npm run test:secretsbefore committing local secret changes.
If images are broken in received email:
- Confirm the image path is site-hosted under
/assets/images/.... - Confirm the deployed public site can serve that path.
- Avoid localhost-only URLs in email payloads.
If localized copy is missing:
- Run
npm run test:i18n. - Check
_data/i18n/{lang}.ymlfor the missing key. - Confirm the Worker can fetch
SITE_BASE/assets/i18n.jsonor hasI18N_CATALOG_JSONin tests.