04-tooling

Resend integration for sc.raydata.co — v0.1 setup

Thu Apr 23 2026 20:00:00 GMT-0400 (Eastern Daylight Time) ·status: live
toolinginfrastructuresanity-checkresendemaildecisions

Resend integration for sc.raydata.co — v0.1

Email capture + welcome transactional + drip scaffolding for Sanity Check Volume II. Shipped 2026-04-24 as the foundation for newsletter distribution. Replaces “RSS only” as the primary subscriber lever.

State of play (current)

ItemStatusDetail
Resend accountactiveAPI key in 1P → Ray AgentResend Ray API Key
Sending domainverifiedraydata.co was already verified in Resend (set up 2026-03-16). DKIM/SPF/return-path live. No DNS work needed today.
Sending addressSanity Check <sanity@raydata.co>Reply-to: ben@raydata.co
AudiencecreatedSanity Check Subscribers — id f603f3ce-3f52-4ae1-8a91-c34492afb33c
Capture endpointlivePOST https://sc.raydata.co/api/subscribe (Cloudflare Pages Function)
Capture formlivesrc/components/SubscribeForm.astro — wired into Footer (every page), homepage post-Vol-II-grid, and /letter
Welcome emailliveFires from the same Function after audience-add succeeds
Drip sequencescaffoldedPlaceholders only; automation NOT wired (see “What’s left for v0.2”)

Decision log

Sending domain: raydata.co (NOT sc.raydata.co)

Picked: raydata.co as the sending domain, with sanity@raydata.co as the From address. Reasoning:

Reversible — if Sanity Check blast volume ever threatens reputation isolation, we can stand up sc.raydata.co as a dedicated sending domain in Resend later, swap the From address, and the raydata.co parent keeps its untouched reputation.

Audiences vs Broadcasts: chose Audience for v0.1, Broadcasts for v0.2

Resend offers both:

v0.1 flow is audience-only: subscribe → add to audience → fire welcome transactional email. Sunday issues will be sent as Broadcasts in v0.2 — composed in Resend, targeted at the audience. The drip sequence (day 3 / 7 / 14) is the open question; see v0.2 below.

Single-step subscribe (no double-opt-in) for v0.1

Per founder direction. Compliance review can come later if SC ever crosses an audience size or geography threshold (GDPR confirmed-opt-in expectations) where double-opt-in is required. For ~hundreds of mostly-US subscribers, single-step is the right starting point.

Files & paths

PathPurpose
~/rdco-sc/functions/api/subscribe.tsCloudflare Pages Function — POST /api/subscribe, validates email, adds to audience, fires welcome email
~/rdco-sc/src/components/SubscribeForm.astroReusable form component (variant="footer" and variant="card")
~/rdco-sc/src/components/Footer.astroIncludes <SubscribeForm variant="footer" /> so every page has subscribe
~/rdco-sc/src/pages/index.astroCard variant inserted between Vol. II grid and Vol. I back-catalog card
~/rdco-sc/src/pages/letter.astroCard variant at the bottom of the editor’s letter
~/rdco-sc/emails/Drip-email templates (welcome live, day-3/7/14 placeholders)
~/rdco-sc/emails/README.md”How to upgrade a placeholder to live” instructions

Endpoint contract

POST /api/subscribe

Request body (JSON):

{ "email": "you@example.com", "source": "homepage" }

Response (200):

{ "ok": true, "welcomeSent": true }

Response (400 — invalid email or already-subscribed conflict that Resend flags):

{ "ok": false, "error": "That email address looks off." }

Response (502 — Resend upstream failure):

{ "ok": false, "error": "..." }

Where the API key lives

SurfaceLocation
1PasswordRay Agent vault → Resend Ray API Keycredential field
Cloudflare Pages — productionenv var RESEND_API_KEY (secret_text) on sc-raydata-co project
Cloudflare Pages — previewsame — set on the preview deployment_config too
Local dev~/rdco-sc/.env.local if/when needed (gitignored) — pull with op read at session start

The audience id (RESEND_AUDIENCE_ID = f603f3ce-3f52-4ae1-8a91-c34492afb33c) is set as a plain_text env var on both Pages configs. Not secret, but stored alongside for parity.

How to add a new drip email

The structural pattern (when the placeholders graduate to live):

  1. Author the body in ~/rdco-sc/emails/<name>.md (or .ts for richer logic). Frontmatter: status, dayOffset, trigger, subject, from, replyTo.
  2. Decide the trigger — see ~/rdco-sc/emails/README.md for the three options (immediate-on-action, scheduled-via-broadcast, scheduled-via-cron).
  3. If immediate — wire into a Pages Function that fires the email via Resend /v1/emails, modeled on the sendWelcome() helper in functions/api/subscribe.ts.
  4. If scheduled — recommended path is a Cloudflare Cron Trigger that wakes daily, queries the audience for subscribers whose joined_at matches today minus the dayOffset, and sends each their targeted transactional email. Audience tagging via the Resend contacts API supports this.
  5. Update status: live in the email’s frontmatter.
  6. Update this doc with the new state.

How to swap audiences

If we ever segment (e.g. “Sanity Check Practitioners” vs “Sanity Check Leaders”):

  1. Create new audience via Resend API (POST /v1/audiences).
  2. Update RESEND_AUDIENCE_ID on the Pages project (both prod + preview) via ~/.claude/scripts/cloudflare-api.sh PATCH to the project endpoint.
  3. The form posts to /api/subscribe either way; the Function reads the env var at request time, so the swap takes effect on the next deploy (or env var change — env changes don’t require a redeploy on Pages).

Gotchas hit during setup

  1. Domain was already verified — saved ~30 min of DNS work. The founder set up Resend on 2026-03-16 with raydata.co already verified. Always check /v1/domains before reaching for Cloudflare DNS records.

  2. Cloudflare Pages production_branch shows as null — even though the project has a “main” branch label on deployments, the actual deploys happen via direct wrangler pages deploy dist calls (ad-hoc deployments). There’s no GitHub integration auto-deploying. This means any branch can ship to production if you run wrangler from it — the source of truth is what’s in dist/ at deploy time.

  3. Resend contacts endpoint is idempotent on email — if a subscriber re-submits, the API returns 200 with the same contact id rather than erroring. This means the form’s “you’re in” state is correct even on re-submit. We do NOT need to detect “already subscribed” on the client.

  4. Astro is static — no SSR adapter — Cloudflare Pages Functions live at ~/rdco-sc/functions/ (project root, sibling of src/), NOT inside the Astro src tree. Pages picks up the directory automatically at deploy time. No astro.config.mjs change needed.

  5. The welcome email uses inline HTML, NO web fonts — email clients (especially Outlook) silently strip or break web fonts and external stylesheets. Body uses system font stack with Georgia fallback for the serif moment.

What’s left for v0.2 (3-line summary)

  1. Sunday-issue Broadcast pipeline — author the issue email template (HTML+plain), use Resend Broadcasts targeted at the Sanity Check Subscribers audience, manually trigger from Resend UI on Sundays until we automate. Highest-leverage next move.
  2. Drip-sequence automation — Cloudflare Cron Trigger that fires day-3 / day-7 / day-14 emails based on contact joined_at tags. The placeholder bodies in ~/rdco-sc/emails/ need real copy first.
  3. Double-opt-in if compliance demands — add a confirmation-link step before the contact lands in the audience. Only if subscriber geography or audience size makes it required. Not blocking v0.2.

Smoke test

Logged separately in this doc when v0.1 ships — see “Smoke test” section below.

Smoke test (2026-04-24 — preview deployment)

Preview URL: https://relaunch-sc-vol-ii.sc-raydata-co.pages.dev Direct deployment: https://006fb996.sc-raydata-co.pages.dev

Promotion to production

Deployed to preview only (aliased at relaunch-sc-vol-ii.sc-raydata-co.pages.dev). Production promotion runs via:

cd ~/rdco-sc && bun run build && NOTES=$(op item get "CloudFlare - claude-rdco" --vault "Ray Agent" --fields label=notesPlain --reveal) && \
  export CLOUDFLARE_API_TOKEN=$(printf '%s\n' "$NOTES" | awk -F': ' '/api token/{gsub(/[^a-zA-Z0-9_-]/, "", $2); print $2; exit}') && \
  export CLOUDFLARE_ACCOUNT_ID=5d4a0efe3ae671cf9cf3265eb1ff738c && \
  wrangler pages deploy dist --project-name sc-raydata-co --branch main --commit-dirty=true

The founder runs this to promote. I did NOT auto-promote because the sandbox blocked the auto-approved production deploy — right call, I’d rather the founder confirm the welcome copy first before it goes live-site.