Build Mac AppDocsMenu

Features

Email: what is sent, and making sure it arrives

Every email this product sends is a template in site/lib/email/templates.tsx, drawn in one layout (site/lib/email/layout.tsx) and sent through Resend (site/lib/email/send.ts). With pnpm dev running, open http://localhost:3000/dev/emails to see each one exactly as sent, and its plain-text part.

What is sent, and by what

EmailKindSent by
Receipt (with the licence key when the provider hands it back)transactionalthe payment webhook, on a paid order
Repository invitation / pendingtransactionalthe github-invite deliverable (docs/deliverables.md)
Your licence keystransactional/recover
You're on the listtransactional/api/subscribe, when RESEND_SEGMENT_ID is set
On salebroadcastpnpm broadcast on-sale
Refund confirmationtransactionalthe payment webhook, on a refund
Release notesbroadcastpnpm broadcast release

site/lib/email/catalogue.ts is the same list in code; the dev preview and the tests walk it, so an email cannot exist without being previewable and tested.

The rules every email keeps

They are written at the top of layout.tsx, and templates.test.ts checks each one on every email:

  • tables and inline styles only;
  • a light background, always;
  • no remote images;
  • a plain-text part, always;
  • both parts leave as ASCII;
  • only the EMAIL palette colours;
  • the seller and the support address in every message;
  • an unsubscribe link in broadcasts, and only in broadcasts.

Sending twice is not possible (by design)

Webhooks retry. Every send carries a stable reference (receipt:<order>, recover:<address>, …), used twice:

  • as Resend's idempotency key, so a repeat within Resend's window is accepted and not delivered;
  • as X-Entity-Ref-ID, so clients thread the rare duplicate that gets through.

Degradation

  • No RESEND_API_KEY: every email is skipped and logged, and the payment still settles. Both payment providers email the licence key themselves too.
  • No RESEND_SEGMENT_ID: the waitlist logs addresses instead of keeping them, and sends no confirmation.

A send failure never fails a webhook: sends run as effects, outside the answer the provider gets.

Deliverability: set this up once, before launch

Mail from a new domain lands in spam until the receiving servers can prove it is really yours. Resend walks you through it; what matters:

  1. Send from a subdomain, e.g. mail.your-domain.com (Resend → Domains → Add). This keeps the sending reputation separate from the domain that receives your mail, so one never drags down the other, and it means the records below never collide with your inbox provider's records on the apex.
  2. SPF and DKIM: add exactly the records Resend shows (an MX and a TXT for SPF on the subdomain, a TXT for DKIM). Wait for Resend to show Verified.
  3. DMARC on the apex: a TXT record at _dmarc.your-domain.com. Start with v=DMARC1; p=none; rua=mailto:you@your-domain.com to receive reports; move to p=quarantine once a few weeks of reports show only your own senders.
  4. RESEND_FROM = Your App <hello@mail.your-domain.com>. Replies still go to SITE.supportEmail (every send sets replyTo), so a customer who answers a receipt reaches a person.
  5. Test with mail-tester.com: send one receipt to the address it gives you (with pnpm dev and a test purchase), and aim for 9/10 or better. Then open one in Gmail and one in Apple Mail, light and dark mode.

Adding an email

/add-email (.claude/skills/add-email/SKILL.md): a function in templates.tsx, an entry in catalogue.ts, a call site that passes a stable refId, and the tests run themselves.

This page is docs/email.md in the repository, copied 2026-09-25.