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)
| Item | Status | Detail |
|---|---|---|
| Resend account | active | API key in 1P → Ray Agent → Resend Ray API Key |
| Sending domain | verified | raydata.co was already verified in Resend (set up 2026-03-16). DKIM/SPF/return-path live. No DNS work needed today. |
| Sending address | Sanity Check <sanity@raydata.co> | Reply-to: ben@raydata.co |
| Audience | created | Sanity Check Subscribers — id f603f3ce-3f52-4ae1-8a91-c34492afb33c |
| Capture endpoint | live | POST https://sc.raydata.co/api/subscribe (Cloudflare Pages Function) |
| Capture form | live | src/components/SubscribeForm.astro — wired into Footer (every page), homepage post-Vol-II-grid, and /letter |
| Welcome email | live | Fires from the same Function after audience-add succeeds |
| Drip sequence | scaffolded | Placeholders 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:
raydata.cowas already verified in Resend (so zero DNS work today).- Brand recognition lives at the umbrella, not the subdomain — recipients
recognize
@raydata.cofaster than@sc.raydata.co. - Gives the founder one shared sender reputation across raydata.co surfaces (HQ, marketing, ops emails). The cost of co-mingling reputation is low because everything originating from raydata.co is hand-authored or semi-personal — no transactional spam risk.
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:
- Audiences — durable contact list with tags. Good for “give me a list of people who match X.” The capture endpoint adds to the audience.
- Broadcasts — campaign-style scheduled email blasts targeted at an audience. Good for the Sunday issue send.
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
| Path | Purpose |
|---|---|
~/rdco-sc/functions/api/subscribe.ts | Cloudflare Pages Function — POST /api/subscribe, validates email, adds to audience, fires welcome email |
~/rdco-sc/src/components/SubscribeForm.astro | Reusable form component (variant="footer" and variant="card") |
~/rdco-sc/src/components/Footer.astro | Includes <SubscribeForm variant="footer" /> so every page has subscribe |
~/rdco-sc/src/pages/index.astro | Card variant inserted between Vol. II grid and Vol. I back-catalog card |
~/rdco-sc/src/pages/letter.astro | Card 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
| Surface | Location |
|---|---|
| 1Password | Ray Agent vault → Resend Ray API Key → credential field |
| Cloudflare Pages — production | env var RESEND_API_KEY (secret_text) on sc-raydata-co project |
| Cloudflare Pages — preview | same — 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):
- Author the body in
~/rdco-sc/emails/<name>.md(or.tsfor richer logic). Frontmatter:status, dayOffset, trigger, subject, from, replyTo. - Decide the trigger — see
~/rdco-sc/emails/README.mdfor the three options (immediate-on-action, scheduled-via-broadcast, scheduled-via-cron). - If immediate — wire into a Pages Function that fires the email via
Resend
/v1/emails, modeled on thesendWelcome()helper infunctions/api/subscribe.ts. - If scheduled — recommended path is a Cloudflare Cron Trigger that
wakes daily, queries the audience for subscribers whose
joined_atmatches today minus the dayOffset, and sends each their targeted transactional email. Audience tagging via the Resend contacts API supports this. - Update
status: livein the email’s frontmatter. - Update this doc with the new state.
How to swap audiences
If we ever segment (e.g. “Sanity Check Practitioners” vs “Sanity Check Leaders”):
- Create new audience via Resend API (
POST /v1/audiences). - Update
RESEND_AUDIENCE_IDon the Pages project (both prod + preview) via~/.claude/scripts/cloudflare-api.sh PATCHto the project endpoint. - The form posts to
/api/subscribeeither 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
-
Domain was already verified — saved ~30 min of DNS work. The founder set up Resend on 2026-03-16 with
raydata.coalready verified. Always check/v1/domainsbefore reaching for Cloudflare DNS records. -
Cloudflare Pages production_branch shows as
null— even though the project has a “main” branch label on deployments, the actual deploys happen via directwrangler pages deploy distcalls (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 indist/at deploy time. -
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.
-
Astro is static — no SSR adapter — Cloudflare Pages Functions live at
~/rdco-sc/functions/(project root, sibling ofsrc/), NOT inside the Astro src tree. Pages picks up the directory automatically at deploy time. No astro.config.mjs change needed. -
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)
- Sunday-issue Broadcast pipeline — author the issue email template
(HTML+plain), use Resend Broadcasts targeted at the
Sanity Check Subscribersaudience, manually trigger from Resend UI on Sundays until we automate. Highest-leverage next move. - Drip-sequence automation — Cloudflare Cron Trigger that fires
day-3 / day-7 / day-14 emails based on contact
joined_attags. The placeholder bodies in~/rdco-sc/emails/need real copy first. - 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
- POST
/api/subscribewith{email: "ben+sctest@raydata.co"}→{ok: true, welcomeSent: true} - Contact confirmed in audience via Resend API (id
ea74c9ef-1909-4398-a792-a422a1b987b6) - Welcome email
last_event: deliveredper Resend/v1/emails— landed in Inbox (not spam) withraydata.coDKIM-signed - Bad-input validation: POST with
{email: "not-an-email"}→ 400{ok: false, error: "That email address looks off."}as expected - Form renders on
/,/letter, and the shared footer across every page
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.
Related vault notes
~/rdco-vault/04-tooling/2026-03-29-infrastructure-decisions.md— domain ownership lineage~/.claude/skills/sanity-check-design/README.md— voice + visual spec for email templates~/.claude/skills/ray-data-co-design/README.md— umbrella voice attributes