Cold Outreach — No-Website Church → Pro Website Pitch
Status: DRAFTED
Owner: John Moelker
Last verified against prod: TBD — verification pass is the first thing to do after this doc lands.
Related skill: church-outreach (plugin skill at ~/.claude/skills/church-outreach/)
Upstream spec: docs/superpowers/specs/2026-04-12-no-website-church-outreach-design.md
Upstream plan: docs/superpowers/plans/2026-04-12-no-website-church-outreach.md
1. Purpose
Send warm, researched cold emails from john@pewsearch.com to 739 PewSearch-visible churches that have no website on file. Primary gift: free AI Starter Kit download (normally $4.95). Secondary offer: PewSearch Pro Website bundle ($19.95/mo). Do this without harming sender reputation and without violating CASL / CAN-SPAM.
Success criteria (measured weekly):
- Delivery rate ≥ 95% (of attempted sends, how many didn't bounce)
- Open rate ≥ 25% (by link-click proxy — no tracking pixel)
- Starter-kit download rate ≥ 10% of deliveries
- Pro Website click-through ≥ 5% of kit downloads
- Unsubscribe rate ≤ 1% (kill-switch at 3% rolling 24h)
- Reply rate (any category) ≥ 3%
2. Trigger
Not a single trigger — a multi-step workflow:
| Step | Trigger type | Who/what |
|---|---|---|
| Campaign load | Manual (founder action) | Founder runs /church-outreach research 20 locally — invokes skill; populates outreach_contacts rows |
| Draft approval | Manual (founder UI) | Founder reviews draft cards at churchwiseai.com/founder/<token>/outreach |
| Gmail draft creation | API call from UI | POST /api/founder/outreach/drafts/[id]/gmail-draft |
| Actual send | Manual (founder clicks Send in Gmail) | Gmail web UI — not automated in Week 1 |
| Sent-status sync | Cron (Week 2+) | /api/cron/outreach-gmail-poll polls Gmail for drafts that became "sent" |
| Reply ingestion | Cron or webhook | Reply-triage cron (TODO — not yet impl, manual in Week 1) |
| Landing page hit | Recipient click | GET /starter-kit/{token} on pewsearch.com |
| Unsubscribe | Recipient click | POST /api/outreach/unsubscribe/{token} (see unsubscribe-and-dnc-gating.md) |
3. Preconditions
Before any send:
-
outreach_campaignsrow exists withname='no-website-churches-2026-04'andstatus='active'. - Target list populated: ~739 rows in
outreach_contactswithcampaign_id=<this>, each linked to achurches.idwheredirectory_visible=true AND (website IS NULL OR website='') AND email IS NOT NULL. - Each contact row has a non-null
starter_kit_token(HMAC, 30-day expiry). - Sender domain
pewsearch.compasses SPF + DKIM + DMARC. Verify withdigormail-tester.com. - Google Workspace "Send as" alias
john@pewsearch.comis active onjohn@churchwiseai.com. - Gmail API OAuth scopes granted to the founder session:
gmail.compose,gmail.modify,gmail.readonly. UI falls back to 428 response withauthorize_urlif missing. -
churches.email_do_not_contactcolumn exists and isfalsefor targeted churches (seeunsubscribe-and-dnc-gating.mdfor how DNC is enforced). - Daily cap not yet exceeded (Week 1: 20/day).
- Kill-switch not triggered (
/api/founder/outreach/kill-switchreturnstriggered: false).
4. Steps
Step 1 — Load campaign + research contacts
What happens: Founder runs the church-outreach skill which (a) queries PewSearch for eligible churches, (b) inserts outreach_contacts rows with status='queued', (c) dispatches parallel Claude sub-agents to research each church (WebSearch + WebFetch across Google Business, denominational directory, news), (d) writes structured research JSON back to outreach_contacts.research_json and moves status to researched.
Where: Skill at ~/.claude/skills/church-outreach/ (plugin) → invokes Supabase MCP for inserts → dispatches parallel Agent tool calls for research.
Verifications:
- db:
SELECT COUNT(*) FROM outreach_contacts WHERE campaign_id='<id>' AND status='queued'after load → expected batch size. - db:
SELECT COUNT(*), jsonb_typeof(research_json) FROM outreach_contacts WHERE campaign_id='<id>' AND status='researched' GROUP BY 2→ expect allobject, count = batch size. - db:
SELECT COUNT(*) FROM outreach_contacts WHERE starter_kit_token IS NULL AND campaign_id='<id>'→ 0 (every row must have a token). - code:
churchwiseai-web/src/test/unit/outreach-queries.test.tsasserts query shapes.
Failure signal: Research sub-agent timeout → contact stuck at researching. Cron should reap stuck rows after 30 min (TODO).
Step 2 — Draft email copy
What happens: Drafting sub-agent reads research_json + church data, produces draft_subject + draft_body. Copy passes quality gate (6 checks, see spec §164). Status → drafted.
Where: Skill drafter → writes to outreach_contacts.draft_subject, draft_body, subject_pattern (A/B/C), personalization_json.
Verifications:
- code: Copy-quality-gate unit test — must reject any draft containing banned phrases (
/hope this (email|finds)/i,/circle back/i, etc.) or missing unsubscribe link. - render: For every drafted row, extract hrefs from
draft_body. Assert: (1) ≥1 link topewsearch.com/starter-kit/<token>, (2) exactly 1 link topewsearch.com/outreach/unsubscribe/<token>, (3) token matchesoutreach_contacts.starter_kit_token, (4) footer contains literal address125 Concession Street, Ingersoll, ON N5C 1G2. TODO: extende2e/email-link-audit.spec.tsto cover outreach drafts — currently only covers lifecycle emails. - db:
SELECT COUNT(*) FROM outreach_contacts WHERE campaign_id='<id>' AND status='drafted' AND (draft_subject IS NULL OR draft_body IS NULL)→ 0. - manual: Spot-check 3 random drafts each morning of Week 1 — does this read like a real pastor wrote it?
Failure signal: Draft rejected → goes back to drafter with feedback. Never reaches founder.
Step 3 — Founder reviews + approves
What happens: Founder opens churchwiseai.com/founder/<token>/outreach. UI lists drafted cards (church name + city + subject + body preview). Founder clicks a card → right pane shows full editable draft + collapsible research + church-record panels. Founder takes one of four actions:
| Action | Endpoint | DB effect |
|---|---|---|
| Approve | POST /api/founder/outreach/drafts/[id]/approve | status = 'approved' |
| Edit & save | PATCH /api/founder/outreach/drafts/[id] with subject/body + note | Updates draft, appends edit_history jsonb entry |
| Skip & re-draft | POST /api/founder/outreach/drafts/[id]/skip with reason | status = 'do_not_contact', skip_reason set |
| DNC forever | Same skip endpoint with reason manual_founder | Also sets churches.email_do_not_contact=true |
Where: UI at churchwiseai-web/src/app/founder/[token]/outreach/OutreachWorkspace.tsx.
Verifications:
- code: Playwright test hitting the outreach workspace with a founder token — approve, edit, skip flows each assert the correct status transition in DB.
- db: After approve:
SELECT status, approved_at FROM outreach_contacts WHERE id='<id>'→'approved', <timestamp>. - db: Edit-history integrity:
SELECT jsonb_array_length(edit_history) FROM outreach_contacts WHERE id='<id>'equals number of PATCHes made. - dashboard: Journey runner persona "skeptical founder" clicks through all 4 actions — UI must be usable without reading docs.
Failure signal: Skip reason missing → validation error (400). Token invalid → 401.
Step 4 — Create Gmail draft
What happens: Founder clicks "Create Gmail draft" on an approved card. UI POSTs /api/founder/outreach/drafts/[id]/gmail-draft. Server:
- Checks
churches.email_do_not_contact— if true, returns409 { error: 'dnc' }and marks contactdo_not_contact. - Checks kill-switch (rolling 24h unsub rate < 3%). If triggered, returns
423 { error: 'kill_switch' }. - Validates Gmail OAuth scopes. If missing, returns
428 { authorize_url: '...' }. - Constructs MIME message with
From: John Moelker <john@pewsearch.com>, subject, HTML body, List-Unsubscribe header (TODO — header not yet set), labeloutreach-2026-04. - Calls
gmail.users.drafts.create→ getsdraft_id. - Writes
outreach_contacts.gmail_draft_id,status='scheduled',scheduled_at=now().
Where: churchwiseai-web/src/app/api/founder/outreach/drafts/[id]/gmail-draft/route.ts
Verifications:
- code: Contract test
outreach-gmail-route-contract.test.ts— DNC check, kill-switch check, 428 scope-missing, 200 happy path all asserted. - db: After success:
SELECT status, gmail_draft_id, scheduled_at FROM outreach_contacts WHERE id='<id>'→'scheduled', <draft_id_string>, <timestamp>. - delivered (in draft sense): Gmail MCP
gmail_list_draftswith querylabel:outreach-2026-04→ draft exists with matching subject. - click: Inspect the draft HTML via
gmail_read_message— assert hrefs resolve correctly (seeunsubscribe-and-dnc-gating.mdStep 1 for the link audit pattern).
Failure signal: 428 → UI prompts re-auth. 423 → UI shows kill-switch banner. 409 → contact row marked DNC, founder sees "already unsubscribed" message.
Step 5 — Founder sends the draft from Gmail
What happens: Founder opens Gmail, reviews draft once more, clicks Send. Gmail dispatches SMTP via Google's outbound infrastructure. Message lands in Sent folder and (hopefully) recipient's Primary inbox.
Where: Gmail web UI — outside our code. This is the one manual step in Week 1.
Verifications:
- delivered: Gmail MCP
gmail_search_messageswithfrom:me to:<recipient> subject:"<draft subject>" label:sent newer_than:1h→ 1 message. The exact query is stored in the contact'sverify_sent_queryfield (TODO — add this column). - delivered: Separate test send to
test+flows@churchwiseai.comat start of each campaign day — must land in Primary (not Promotions/Spam). manual: screenshot saved toknowledge/tests/artifacts/outreach-inbox-placement/YYYY-MM-DD.png. - TODO: no automated "did it send" check in Week 1. Founder must manually click Send. Risk: founder gets distracted, draft sits unsent for days, pastor is confused when the follow-up references an email that never arrived. Mitigation: daily audit cron counts drafts older than 24h in
scheduledstatus and alerts founder.
Failure signal: Message bounces → see Step 8. Message lands in Promotions/Spam → reputation issue, pause campaign, investigate.
Step 6 — Cron detects "sent" state
What happens: /api/cron/outreach-gmail-poll runs every hour. For each contact with status='scheduled', polls Gmail API to check if the draft became a sent message:
- If yes →
status='sent',sent_at=<message_internalDate>, cleargmail_draft_id. - If no → leave alone.
- If draft deleted without send →
status='approved'(unsend) with alert.
Where: churchwiseai-web/src/app/api/cron/outreach-gmail-poll/route.ts
Status: STUB (Week 1) — full implementation due Wed Apr 15.
Verifications:
- cron:
curl -H "Authorization: Bearer $CRON_SECRET" $BASE_URL/api/cron/outreach-gmail-poll→ 200 with summary JSON ({polled: N, marked_sent: M, errors: E}). - db:
SELECT COUNT(*) FROM outreach_contacts WHERE status='scheduled' AND scheduled_at < now() - interval '48 hours'→ 0 after cron has had time to process (any rows > 48h scheduled likely mean founder never sent). - TODO: Add Sentry/console alert when cron detects draft deletion without send.
Failure signal: Cron 500 → Vercel cron monitor fires, founder paged. Cron silently no-ops (returns 200 with polled: 0 when drafts exist) → no alert today, gap.
Step 7 — Pastor receives email & clicks CTA
What happens: Pastor opens email (or doesn't). If they click:
- Unsubscribe link → Step 8
- Starter-kit link → Step 9
- Pro Website link on landing page → Step 10
Verifications:
- click: For one canonical email per campaign, via Gmail MCP fetch
test+flows@churchwiseai.comcopy, extract every href, Playwrightrequest.fetcheach one — assert valid destination (not 404, not redirect to/, not?auth=invalid). The general pattern lives inchurchwiseai-web/e2e/email-link-audit.spec.ts; TODO: addoutreach-link-audit.spec.tsthat pulls sample fromoutreach_contacts.draft_body.
Step 8 — Unsubscribe path
What happens: See unsubscribe-and-dnc-gating.md Steps 1-4. Click → landing page → POST → DB write → DNC flag set → no future emails from any property.
Verifications: (deferred to linked doc)
Failure signal: P0 if a DNC'd pastor receives any further email.
Step 9 — Starter Kit download
What happens: GET /starter-kit/{token} on pewsearch.com (pewsearch/web/src/app/starter-kit/[token]/page.tsx):
- Validate token against
outreach_contacts.starter_kit_token+starter_kit_token_expires_at. - Record
kit_downloaded_at=now()on first visit (idempotent). - Status transitions:
sent → link_clicked → kit_downloaded. - Render page: above-fold = church name + big "Download your AI Starter Kit" button. Below-fold = 1-paragraph Pro Website pitch with "Tell me more" CTA.
Where: pewsearch/web/src/app/starter-kit/[token]/page.tsx + API at pewsearch/web/src/app/api/outreach/kit-download/[token]/route.ts.
Verifications:
- code: Playwright: navigate to
/starter-kit/<valid-token>→ assert church name in hero, download button visible, clicking it returns a PDF with Content-Typeapplication/pdf. - code: Navigate to
/starter-kit/<expired-token>→ assert 404 with kind copy + link to paid kit. - code: Navigate to
/starter-kit/<invalid-token>→ same 404. - db: After download:
SELECT status, kit_downloaded_at FROM outreach_contacts WHERE starter_kit_token='<token>'→'kit_downloaded', <timestamp>. - db: Idempotency — second download must not overwrite
kit_downloaded_at.
Failure signal: Page returns 500 → PDF storage bucket misconfigured, or SUPABASE_SERVICE_ROLE_KEY missing.
Step 10 — Pro Website CTA click
What happens: Pastor clicks "Tell me more about Pro Website" on landing page. Client fires POST /api/outreach/pro-website-click/{token} (non-blocking analytics beacon), then browser navigates to pewsearch.com/pro-website (or /claim/<slug> if church slug known).
Where:
- Click handler:
pewsearch/web/src/app/starter-kit/[token]/page.tsx(client component) - Beacon API:
pewsearch/web/src/app/api/outreach/pro-website-click/[token]/route.ts - Destination:
pewsearch/web/src/app/pro-website/page.tsx(marketing) ORpewsearch/web/src/app/claim/[slug]/page.tsx(if pre-personalized)
Verifications:
- code: Playwright: click the Pro Website CTA on the demo kit page → assert (1) POST fires (intercept network), (2) browser lands on
/pro-websiteor/claim/<slug>, (3) page renders pricing and a "Claim" button. - db: After click:
SELECT status, pro_website_clicked_at FROM outreach_contacts WHERE starter_kit_token='<token>'→'pro_website_clicked', <timestamp>. - click: The landing destination (
/pro-websiteor/claim/<slug>) itself has working CTAs — price cards, "Claim your church" buttons — that don't 404 or redirect home. This is where the Hope Community bug class lives. Run link audit on the destination page.
Failure signal: Beacon POST 404 → analytics gap but doesn't block pastor. Destination 500 → HIGH, pastor intent lost.
Step 11 — Pastor claims Pro Website (conversion path)
What happens: Pastor clicks "Claim" on /claim/<slug> or /pro-website. Follows the standard PewSearch claim flow (GenericClaimPage → Pre-checkout → PreviewPage → Stripe Checkout → Webhook → ActivationSuccessPage). On activation, flow sets outreach_contacts.converted_at=now(), status='converted'.
Where: pewsearch/web/src/app/claim/[slug]/page.tsx → /api/stripe/pre-checkout → Stripe → /api/stripe/webhook.
Verifications:
- code: E2E test
pewsearch/web/e2e/crud-admin-*.spec.tsfamily already covers parts — verify the outreach-attribution hook runs in the webhook. - db: After Stripe webhook fires:
SELECT converted_at, status FROM outreach_contacts WHERE church_id='<converted-church>'→<timestamp>, 'converted'. - db:
SELECT plan, status FROM premium_churches WHERE church_id='<converted-church>'→'pro_website', 'active'. - TODO: Stripe webhook currently does NOT backfill
outreach_contacts.converted_at— it only knows about premium_churches. Need a reverse-lookup hook. Gap: campaign attribution reporting will under-count conversions until this is added.
Failure signal: Webhook fires but outreach_contacts never updated → attribution lost, founder can't prove the campaign worked.
Step 12 — Reply / bounce / no-response
Three terminal branches:
- Reply → Step 13
- Bounce → Step 14
- No response after 14 days → contact stays at
sent, no follow-up in this campaign (Week 3 follow-up email is out-of-scope for this doc).
Step 13 — Reply triage
What happens: Reply-triage cron (TODO — not yet built) reads inbox via Gmail API push, classifies with Haiku 4.5, writes replied_at + reply_category, takes action:
send-kit-link-again→ Resend auto-responder with kit URL.question-about-product→ Flag for founder.not-interested→ Markstatus='do_not_contact'.unsubscribe-request→ Markstatus='unsubscribed'+ setchurches.email_do_not_contact=true.
Status: TODO. Week 1 process: founder reads replies in Gmail manually.
Verifications: All currently manual. Weekly audit: count replies in Gmail with label:outreach-2026-04 label:replies vs outreach_contacts.replied_at IS NOT NULL — must match.
Step 14 — Bounce handling
What happens: Gmail soft-bounces retry automatically. Hard bounces return delivery-status notifications to john@pewsearch.com. Resend's feedback loop at send.pewsearch.com also captures hard bounces. Both paths should set outreach_contacts.bounced_at + mark churches.email_do_not_contact=true with reason bounce_hard.
Status: TODO. Resend feedback loop webhook not yet wired into a bounce-handling endpoint for the outreach campaign.
Verifications:
- TODO: After bounce,
SELECT bounced_at, status FROM outreach_contacts WHERE email='<bounced-addr>'→<timestamp>, 'bounced'. - TODO: Verify
churches.email_do_not_contact=truewithemail_do_not_contact_reason='bounce_hard'. - manual (Week 1): Founder reads bounce notifications in Gmail, runs update SQL by hand.
Step 15 — Kill-switch monitoring
What happens: Rolling 24h unsub rate is computed by /api/founder/outreach/kill-switch. If > 3%, subsequent Gmail-draft-create calls return 423. UI banner tells founder "Campaign paused — unsub rate 4.2%, review before resuming."
Where: churchwiseai-web/src/app/api/founder/outreach/kill-switch/route.ts + churchwiseai-web/src/app/founder/[token]/outreach/KillSwitchBanner.tsx.
Verifications:
- code: Unit test
churchwiseai-web/src/test/unit/outreach-funnel-stats.test.ts— simulate 100 sent + 4 unsub → triggered=true. - db:
SELECT COUNT(*) FILTER (WHERE unsubscribed_at > now() - interval '24 hours') * 100.0 / COUNT(*) FILTER (WHERE sent_at > now() - interval '24 hours') AS unsub_rate FROM outreach_contacts→ matches the API response. - dashboard: Load founder workspace — if triggered, banner visible with accurate percentage.
Failure signal: Kill-switch stuck on → founder can't send even after resolving root cause. Admin override endpoint needed (TODO).
5. What the recipient sees
Sample (from spec §244, with real values substituted):
From: John Moelker <john@pewsearch.com>
Subject: Greater Nazaree's listing on PewSearch
Hi Pastor Williams,
I noticed Greater Nazaree has been serving Franklin for over 40 years —
your Easter sunrise service on the riverfront came up in the Franklin
Observer's community events calendar.
We also noticed your listing on PewSearch doesn't link to a website.
That's not unusual — lots of faithful churches are focused on people,
not pixels. But we built something that might help the people who
Google you before they ever visit.
It's called the AI Starter Kit — a PDF playbook + 12 prompts for
pastoral use. Normally $4.95 on our site. Here's a free copy for
Greater Nazaree: https://pewsearch.com/starter-kit/abc123-xyz456
(If you'd like a simple website that shows up when people search
"church near me" in Franklin, we have a $19.95/mo option — the kit
page has the details.)
Thanks for what you do.
— John
Founder, ChurchWiseAI + PewSearch
---
ChurchWiseAI LTD · 125 Concession Street, Ingersoll, ON N5C 1G2
You're receiving this because Greater Nazaree is listed at
pewsearch.com/churches/greater-nazaree-franklin-tn.
Don't want these emails? Unsubscribe in one click:
https://pewsearch.com/outreach/unsubscribe/abc123-xyz456
6. Compliance & unsubscribe
- Regime: Both CAN-SPAM (US recipients) and CASL (Canadian sender). Stricter wins per message.
- Implied consent basis (CASL): Pastor's email is "conspicuously published" on Google Business profile for the pastoral role; topic is directly relevant. Valid 24 months.
- Required elements (every email must have all 6):
- ✅ Sender name + from address accurate (
John Moelker <john@pewsearch.com>) - ✅ Relationship disclosure (
"listed at pewsearch.com/churches/{slug}") - ✅ One-click unsubscribe link with per-contact token
- ✅ Physical mailing address in footer (
125 Concession Street, Ingersoll, ON N5C 1G2) - ✅ Non-misleading subject line
- TODO:
List-UnsubscribeandList-Unsubscribe-Postheaders for one-click Gmail UI button
- ✅ Sender name + from address accurate (
- DNC gate:
churches.email_do_not_contactchecked in Step 4 (Gmail draft creation). Enforcement sites catalog inunsubscribe-and-dnc-gating.md§4.
7. Failure modes
| Failure | Signal | Alert / handling |
|---|---|---|
Draft sits scheduled > 48h (founder forgot to send) | Daily audit cron | TODO — not yet built |
| Gmail cron returns 200 but marks nothing sent when drafts exist | Silent no-op | gap — add metric |
| Pastor clicks unsub after 30 days (token expired) | 404 on unsub page | HIGH — FIX THIS WEEK per unsubscribe-and-dnc-gating.md §9 gap #2 |
| Starter-kit PDF link on landing page 404s | Founder doesn't notice | Add pre-campaign smoke test of PDF download |
Pro Website CTA destination /pro-website or /claim/<slug> has broken links (same class as Hope Community lifecycle bug) | Silent — pastor bails | Run e2e/email-link-audit.spec.ts pattern against these pages monthly |
| Kill-switch triggered → founder unaware | Banner must be visible | Dashboard loads render banner before any draft list |
| MX / SPF / DKIM misconfigured → all emails spam-foldered | Week 1 inbox-placement test catches | Test with test+flows@churchwiseai.com each morning |
| Gmail rate-limit hit | 429 from Gmail API | Cap at 20/day Week 1; bump only after clean Week 1 |
Stripe webhook on conversion fails to backfill outreach_contacts.converted_at | Attribution reporting under-counts | TODO — known gap |
8. Verification manifest (summary)
flow: cold-outreach-no-website-pro-website
priority: P0
verifications:
- step: 1
verb: db
command: |
SELECT COUNT(*) FROM outreach_contacts
WHERE campaign_id = '<id>' AND starter_kit_token IS NULL;
expect: 0
- step: 2
verb: render
command: |
For each drafted row, extract hrefs from draft_body.
Assert one /starter-kit/ link, one /outreach/unsubscribe/ link,
physical address footer present.
status: TODO — extend e2e/email-link-audit.spec.ts
- step: 3
verb: code
command: |
pnpm -C churchwiseai-web playwright test
e2e/outreach-workspace-crud.spec.ts
status: TODO — write spec
- step: 4
verb: code
command: |
pnpm -C churchwiseai-web test -- outreach-gmail-route-contract
expect: All 4 contract tests pass
- step: 4
verb: delivered
command: |
gmail_list_drafts(query="label:outreach-2026-04 newer_than:1h")
expect: 1 draft per recent API call
- step: 5
verb: delivered
command: |
gmail_search_messages(q="from:me label:sent newer_than:24h
to:test+flows@churchwiseai.com subject:<subject>")
expect: 1 message
- step: 8
verb: click
ref: unsubscribe-and-dnc-gating.md#step-2
- step: 9
verb: db
command: |
SELECT status FROM outreach_contacts
WHERE starter_kit_token = '<token>';
expect-after-download: kit_downloaded
- step: 10
verb: click
command: |
After Pro Website CTA click on /starter-kit/<token>:
fetch /pro-website or /claim/<slug>, assert status 200,
assert no broken hrefs in response body (same pattern as
e2e/email-link-audit.spec.ts).
status: TODO
- step: 11
verb: db
command: |
After Stripe checkout success for outreach church:
SELECT converted_at FROM outreach_contacts
WHERE church_id = '<id>';
status: TODO — webhook backfill not yet wired
- step: 15
verb: code
command: |
pnpm -C churchwiseai-web test -- outreach-funnel-stats
expect: kill-switch triggered at 3% threshold
9. Open questions / known gaps (consolidated)
See each step's "Failure signal" and the TODO markers in verifications. Rolled up:
- [P1] Unsub after 30 days returns 404 — token-expiry check should not block unsub (see
unsubscribe-and-dnc-gating.md§9 #2). - [P1] Lifecycle cron doesn't check
churches.email_do_not_contact— cross-property DNC gap. - [P2]
List-Unsubscribeheaders not set on Gmail-API sends. - [P2] Reply triage cron not yet impl — manual process in Week 1.
- [P2] Bounce handling not wired to update
churches.email_do_not_contact. - [P2]
/api/cron/outreach-gmail-pollis a stub — full impl due Wed Apr 15. - [P2] Daily audit of stuck
scheduleddrafts not built. - [P2] Stripe webhook conversion path does not backfill
outreach_contacts.converted_at. - [P3] Pro Website CTA destination (
/pro-website,/claim/<slug>) not yet link-audited. - [P3] No admin override for kill-switch.
10. Go-live gate
This flow is NOT launch-ready until:
-
e2e/outreach-link-audit.spec.tsexists and passes (Step 2 verification). - Unsub-after-expiry bug (gap #1) fixed.
- Lifecycle cron DNC check (gap #2) added.
- Founder has executed end-to-end verification against
test+flows@churchwiseai.comand signed off with date + screenshot inknowledge/tests/artifacts/outreach/.
Flip Status to VERIFIED only when every checkbox above is green.