Skip to main content

Manual Unsubscribe Runbook

When this fires

A recipient REPLIES to outreach (vs clicking the auto-unsubscribe link in the footer) asking to be removed. Phrases vary: "unsubscribe", "remove me", "stop emailing", "please don't contact us again", "not interested", "wrong person".

Two recurring wrinkles make this more than a one-line UPDATE:

  1. Reply-from ≠ sent-to. We typically email info@<church>.org (the church's general inbox). The person who reads it and replies often does so from their personal church address — pastor@, office@, gbowering@. A search on the reply-from address alone misses the row that's actually scheduled.
  2. Duplicate church rows. The churches directory often has 2+ rows at the same domain from different scrape sources. We need to DNC every row at the domain, not just the one the contact pointed to.

Two ways to handle it

Option A — Use the button (preferred)

  1. Go to /founder/<FOUNDER_TOKEN>/outreach-engine
  2. Click ✋ Process Unsubscribe in the header (top right)
  3. Paste the recipient's email address (the one that replied)
  4. Paste their verbatim message (optional but recommended — gets stored in outreach_contacts.founder_notes for the audit trail)
  5. Click Find matches — the modal previews every contact + church row that matches the exact email OR same domain
  6. Verify the matches look right (especially that no unrelated rows are caught in the domain match)
  7. Click Apply DNC to all matches — writes are atomic per row, idempotent on already-DNC'd rows
  8. Reply to the recipient ("Will do, [name].") — the button does NOT send the reply. Email is a human-to-human courtesy.

Option B — Direct SQL (fallback when the UI is down)

-- 1. Mark the outreach contact(s) unsubscribed
UPDATE outreach_contacts
SET status = 'do_not_contact',
unsubscribed_at = now(),
founder_notes = COALESCE(founder_notes || E'\n---\n', '') ||
'Manual unsubscribe via email reply ' || to_char(now(), 'YYYY-MM-DD') ||
' from <REPLY_EMAIL>. Message: "<VERBATIM>"',
updated_at = now()
WHERE email = '<REPLY_EMAIL>'
OR email ILIKE '%@<DOMAIN>';

-- 2. DNC every church row at the same domain
UPDATE churches
SET email_do_not_contact = true, updated_at = now()
WHERE email = '<REPLY_EMAIL>'
OR email ILIKE '%@<DOMAIN>';

Run via Supabase MCP (mcp__plugin_supabase_supabase__execute_sql, project_id wrwkszmobuhvcfjipasi).

CASL / CAN-SPAM compliance notes

  • "Within 10 business days" is the CAN-SPAM clock; CASL is similar. Both patterns above complete in seconds — well inside the window.
  • We never sell, share, or re-add a DNC address. The email_do_not_contact flag on churches is read by every outreach query.
  • Founder reply to the recipient is courtesy, not legally required.

What the API does under the hood

POST /api/founder/outreach/manual-unsubscribe?token=<FOUNDER_TOKEN> with body:

{
"email": "pastor@church.org",
"message": "Please remove us from your list.",
"confirm": true // false = preview, true = apply
}
  • confirm: false returns matches without writing — used by the modal's preview step.
  • confirm: true writes the DNC and appends verbatim context to outreach_contacts.founder_notes (existing notes are preserved with a --- separator).
  • Optional contactIds[] / churchIds[] in the body let the caller narrow the apply scope; default is "all matches."

Audit trail location

  • outreach_contacts.founder_notes — verbatim message + date + reply email
  • outreach_contacts.unsubscribed_at — exact timestamp
  • outreach_contacts.status = 'do_not_contact' — visible in Drafts filter
  • churches.email_do_not_contact = true — blocks all future outreach builds

When to escalate

  • Recipient is angry / threatening legal action → tell founder immediately, do NOT auto-reply. Founder writes the response personally.
  • Recipient is a current paying customer → STOP. Check premium_churches for the church_id first. A paying customer asking to stop outreach is also asking us to stop marketing, not stop service. Confirm scope before flipping any flags.

History

  • 2026-05-18 — Created after gbowering@rivercitychurch.org reply made it clear this workflow was about to recur often as outreach scaled. Pre-button version was: founder pastes message → Claude finds matches → Claude shows SQL → founder confirms → Claude executes. The button collapses that into a single click.