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:
- 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. - Duplicate church rows. The
churchesdirectory 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)
- Go to
/founder/<FOUNDER_TOKEN>/outreach-engine - Click ✋ Process Unsubscribe in the header (top right)
- Paste the recipient's email address (the one that replied)
- Paste their verbatim message (optional but recommended — gets stored in
outreach_contacts.founder_notesfor the audit trail) - Click Find matches — the modal previews every contact + church row that matches the exact email OR same domain
- Verify the matches look right (especially that no unrelated rows are caught in the domain match)
- Click Apply DNC to all matches — writes are atomic per row, idempotent on already-DNC'd rows
- 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_contactflag onchurchesis 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: falsereturns matches without writing — used by the modal's preview step.confirm: truewrites the DNC and appends verbatim context tooutreach_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 emailoutreach_contacts.unsubscribed_at— exact timestampoutreach_contacts.status = 'do_not_contact'— visible in Drafts filterchurches.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_churchesfor 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.