Skip to main content

email-best-practices

Auto-load

Attaches itself automatically when a prompt matches.

Guidance for building deliverable, compliant, user-friendly email flows. Use when the user adds email signup, password reset, magic links, transactional sends, newsletters, contact forms, or asks about spam / SPF / DKIM / DMARC / bounces / CAN-SPAM / GDPR. Vendor-agnostic — concept is the same with Resend, SendGrid, Postmark, AWS SES, or fetch+SMTP.

Scaffolds

next

Required env vars

RESEND_API_KEY

Source

Adapted from resend/resend-skills (email-best-practices, adapted)

Instructions

The full instruction set the agent receives when this skill is active.

Email Best Practices

Most email problems happen at one of three points: setup (auth records wrong), sending (no retry/idempotency), and consequences (no suppression list, no compliance). Get all three right and the system holds.

The Email Lifecycle

[User] → [Form + Validation] → [Double Opt-In] ↓ [Consent Recorded] ↓ [Suppression check] ←─────── [Ready to send] ↓ [Idempotent send + retry] ────→ [Email API / SMTP] ↓ [Webhook events] ↓ ┌────────┬────────┬────────────┐ ↓ ↓ ↓ ↓ Delivered Bounced Complained Opened/Clicked ↓ ↓ [Suppression list updated]

Most "spam folder" problems are actually unauthenticated mail — fix DNS first, not copy.

Deliverability — DNS authentication (do this before sending anything)

Three records, all on the sending domain:

  1. SPF — TXT record listing the IPs/domains allowed to send. Example: v=spf1 include:_spf.resend.com ~all
  2. DKIM — public key the receiver uses to verify the signature. Your provider gives you one or more CNAMEs to add.
  3. DMARC — TXT record at _dmarc.example.com declaring what to do with unauthenticated mail. Start at p=none to monitor, move to p=quarantine or p=reject once clean.

Gmail and Yahoo reject unauthenticated bulk mail outright as of 2024. There is no "soft fail" anymore.

Transactional vs Marketing — pick correctly

TransactionalMarketing
Triggered byA user actionA schedule or campaign
ConsentImplicit from the actionExplicit opt-in required
ExamplesPassword reset, OTP, receipt, account confirmationNewsletter, promo, drip
Unsubscribe linkOptional (not legally required)Required (CAN-SPAM, GDPR, CASL)
List membershipSent regardless of subscriptionSent only to subscribed list

Don't mix the two. A "thanks for signing up — here are 5 ways to use our product" email is marketing, not transactional.

Transactional emails — common catalog

If the user is building auth/payments/comms, plan for these:

  • Email verification (signup)
  • Password reset
  • Magic link / OTP login
  • Order/receipt confirmation
  • Shipping/status update
  • Security alert (new device, password changed)
  • Account deletion confirmation
  • Invoice / payment failure / billing reminder

Build templates once. Reuse them. They're text + a CTA, not an art project.

Capturing emails — form basics

  • Single field, validated client-side AND server-side
  • Honeypot field to defeat dumb bots (<input name="website" tabindex="-1" autocomplete="off" style="display:none">)
  • Server-side disposable-domain check (block mailinator.com, etc.) only if abuse is real
  • Double opt-in: send a confirmation link, only mark subscribed after click. Reduces bounces and spam complaints, required in EU under GDPR

Sending reliability

  • Idempotency key on every send so a retry doesn't double-send (Idempotency-Key: build_42_user_99)
  • Exponential backoff on 5xx responses: 1s, 2s, 4s, 8s, give up after 5 tries and queue for manual review
  • Async — never block the request that triggered the send waiting for the API. Queue and return immediately. If the queue fails, the user retried login isn't blocked
  • Logging — record {messageId, to, template, status, providerResponse} for every send, redacting PII as needed

Webhooks — what to handle

Subscribe to these events from your provider:

EventAction
deliveredUpdate lastDeliveredAt
bounced (hard)Add to suppression list immediately
bounced (soft)Count; suppress after 5 soft bounces in 30 days
complainedAdd to suppression list immediately, unsubscribe from all
unsubscribedAdd to suppression list for that list type

Verify webhook signatures. An unsigned POST to /webhooks/email will get spoofed.

List hygiene

  • Run weekly: prune addresses with no opens in 90 days from marketing lists
  • Run nightly: any new hard-bounce goes into suppression
  • Never email an address on the suppression list — even transactional. If a transactional email truly needs to go out (e.g. a final account-deletion confirmation), document the exception in code

Compliance — quick checklist

LawApplies ifKey requirements
CAN-SPAM (US)Sending commercial mail to US recipientsWorking unsubscribe link, physical address in footer, accurate From and subject
GDPR (EU)Any EU recipient on a marketing listExplicit opt-in consent, right to access/erase, lawful basis recorded
CASL (Canada)Any CA recipientExpress OR implied consent, identification of sender, unsubscribe

Don't make this lawyer territory unless you have real reach. The 3 essentials: opt-in, unsubscribe link, sender address cover most.

Platform notes (Codebase)

  • scaffold-next: send from a Route Handler (app/api/email/send/route.ts). Use a queue (e.g. Upstash QStash or in-process for low volume) so the request doesn't block.
  • Use react-email to template (npm install react-email @react-email/components) — works with Resend, SendGrid, AWS SES, or hand-rolled SMTP.
  • The user MUST supply their own API key as an env var. Never hard-code a key, never invent one.

What NOT to do

  • Send first, ask for consent later — you're already on a deny list by week two
  • Use a noreply@ address as the From — modern inboxes flag these. Use a monitored mailbox
  • Skip the unsubscribe link "because it's transactional" — fine for receipts, but the first time you send marketing, you need one
  • Mix templates between transactional and marketing infrastructure — keep the IP/domain reputation separate

See also: [[security-review]] (don't log raw email contents with credentials).

email-best-practices skill — Codebase