Handoff: Lifecycle Email System — Phase 3 Triggers, QA, KB Wiring
Created: 2026-03-27 Context: Phase 1-2 are live (infrastructure, onboarding, win-back, CWA cross-promo). Phase 3 sequences have templates built but triggers not yet wired. Marketing opt-in compliance added to PewSearch. MailerLite KB updated.
Read These First
C:\dev\knowledge\architecture\lifecycle-email-system.md— FULL architecture spec (sequences, skip conditions, DB schema)C:\dev\knowledge\handoffs\lifecycle-email-system-handoff.md— original Phase 1-2 handoff (completed)C:\dev\knowledge\integrations\mailerlite.md— MailerLite source of truth (updated 2026-03-27)C:\dev\churchwiseai-web\src\lib\lifecycle-emails.ts— the sequence engine (all 11 sequences + 25+ templates)C:\dev\churchwiseai-web\src\app\api\cron\lifecycle-emails\route.ts— the daily cronC:\dev\CLAUDE.md— cross-project rules
What's Already Live
| Component | Status | Branch | How It Works |
|---|---|---|---|
lifecycle_emails_sent table | Live | Supabase | UNIQUE(church_id, email_key) = state machine |
| Cron job | Live | main (CWA) | Daily 8am ET via vercel.json |
| Onboarding (Starter/Pro/Suite) | Live | main | Cron processes days 2/5/7/13/30 for active churches |
| Welcome + Starter Kit emails | Live | main | Webhook fires immediately, logs to sent table |
| Win-back (cancelled churches) | Live | main | Cron processes days 3/14/30 after cancellation |
| CWA → ShareWise cross-promo | Live | main | Cron processes day 7 for active CWA churches |
| Marketing opt-in (PewSearch) | Live | master (PS) | ClaimForm + PastorLeadCapture + API gates |
| MailerLite KB doc | Live | master (KB) | Updated with lifecycle system, compliance, automation status |
Part 1: Wire Phase 3 Triggers
What's missing
Six sequences have templates built in lifecycle-emails.ts but NO trigger wired:
| # | Sequence | Template Keys | Trigger Needed | Where Trigger Lives |
|---|---|---|---|---|
| 4 | Newsletter Welcome | newsletter_welcome, newsletter_why_ai, newsletter_demo | Newsletter signup | CWA /api/newsletter, PS /api/subscribe, ITW /api/newsletter |
| 5 | 7-Day AI Ministry Course | course_day1 through course_day7 | Newsletter signup (concurrent with #4) | Same as above |
| 6 | Kit Buyer Nurture | kit_delivery, kit_followup, kit_upgrade | Starter Kit purchase | CWA Stripe webhook (product === 'ai_starter_kit') |
| 7 | PewSearch → CWA | xpromo_ps_cwa_0, xpromo_ps_cwa_5, xpromo_ps_cwa_14 | PewSearch claim (paid or free) | PS /api/leads/capture + PS Stripe webhook |
| 8 | ITW → SermonWise | xpromo_itw_sw | ITW Premium signup | ITW Stripe webhook or Supabase Auth signup |
| 9 | SermonWise → ITW | xpromo_sw_itw | SermonWise Pro signup | CWA Stripe webhook (product === 'sermon_pro') |
How to wire each trigger
Sequences 4 & 5: Newsletter + Course
Problem: Newsletter subscribers aren't in premium_churches — they're in email_subscribers. The cron currently only queries premium_churches. Non-church sequences need a different data source.
Approach: Add a new section to the cron that queries email_subscribers for recent signups and processes newsletter/course sequences.
Steps:
- The
email_subscriberstable has:id,email,source,tags,status,created_at. Thecreated_atis the signup date — use this as the sequence start date. - For newsletter + course, the
church_idinlifecycle_emails_sentshould use a synthetic ID (e.g., hash of email) since these subscribers aren't churches. - Add a section to the cron route after the church processing:
// 4. Process newsletter + course sequencesconst { data: subscribers } = await supabase.from('email_subscribers').select('id, email, created_at, tags, status').eq('status', 'active').gte('created_at', thirtyDaysAgo); // only process recent signups
- For each subscriber, check
lifecycle_emails_sentusing theiridaschurch_id(repurpose the column for non-church sequences). - Compute
daysSinceSignupfromcreated_at, check schedules for bothNEWSLETTER_WELCOMEandAI_MINISTRY_COURSE. LifecycleEmailPropswon't have church-specific fields — use defaults:churchName: '',pastorName: '',adminToken: ''.- The newsletter/course templates in
renderEmailHtmldon't reference church-specific props, so this works.
Alternative simpler approach: Instead of modifying the cron, fire the immediate emails (day 0) from the signup API routes, and add the subscriber to lifecycle_emails_sent with their subscriber id as church_id. The cron then picks up the follow-ups. This matches how the webhook handles onboarding.
Sequence 6: Kit Buyer Nurture
Problem: The Starter Kit already has a sendStarterKitDeliveryEmail in email.ts fired by the Stripe webhook. The lifecycle system also has kit_delivery, kit_followup, kit_upgrade templates.
Approach: In the Stripe webhook's AI Starter Kit handler (line ~237-257 of webhook/route.ts), after sending the existing delivery email, log kit_delivery to lifecycle_emails_sent using the customer email hash as church_id. The cron handles days 3 and 7.
Steps:
- In the webhook's
if (session.metadata?.product === 'ai_starter_kit')block:- After
sendStarterKitDeliveryEmail, insert intolifecycle_emails_sent:church_id: crypto.randomUUID() // or hash of emailemail: customerEmailemail_key: 'kit_delivery'sequence: 'kit_nurture'
- After
- Add a cron section that queries
lifecycle_emails_sententries withsequence = 'kit_nurture'andemail_key = 'kit_delivery'to find kit buyers, then processeskit_followup(day 3) andkit_upgrade(day 7) based onsent_atof the delivery email.
Design decision needed: The current cron structure uses premium_churches as the source of truth for church sequences and lifecycle_emails_sent as the dedup log. For non-church sequences (newsletter, course, kit), we need a different approach since there's no premium_churches row. Options:
- Option A: Use
email_subscribers.idaschurch_idinlifecycle_emails_sent(repurpose the column) - Option B: Add a nullable
subscriber_idcolumn tolifecycle_emails_sentand query by that - Option C: Use a deterministic UUID derived from the email address (e.g.,
uuid5(email))
Recommendation: Option A is simplest. The church_id column is a UUID — email_subscribers.id is also a UUID. The UNIQUE constraint on (church_id, email_key) still works for dedup. Just name it clearly in comments.
Sequence 7: PewSearch → CWA Cross-Promo
Trigger: When a church claims their PewSearch listing (paid claim via /api/stripe/pre-checkout or free claim via /api/leads/capture).
Steps:
- In PewSearch's
/api/leads/capture(free claim): after creating thepremium_churchesrow, logxpromo_ps_cwa_0tolifecycle_emails_sentvia the CWA lifecycle system. - Problem: PewSearch doesn't have direct access to
sendAndLog(that's in churchwiseai-web). Two options:- Option A (recommended): Add the PewSearch subscriber to
lifecycle_emails_sentdirectly via Supabase (both codebases share the same DB). Logxpromo_ps_cwa_0asimmediate: truefrom the PewSearch API route. The cron picks up days 5 and 14. - Option B: Create a webhook/API in CWA that PewSearch calls to trigger cross-promo sequences.
- Option A (recommended): Add the PewSearch subscriber to
- For paid claims: PewSearch's Stripe webhook also activates the church. Add the same logging there.
- The cron needs a new section to process
crosspromo_ps_cwaentries: querylifecycle_emails_sentwheresequence = 'crosspromo_ps_cwa'andemail_key = 'xpromo_ps_cwa_0', compute days sincesent_at, send follow-ups.
Sequences 8 & 9: ITW ↔ SermonWise Cross-Promo
Trigger: When a user subscribes to ITW Premium or SermonWise Pro.
Steps:
- ITW Premium signup: In
churchwiseai-web/src/app/api/stripe/webhook/route.ts, the ITW Premium checkout is NOT currently handled (ITW uses Supabase Auth, not Stripe for basic subscriptions, but Premium is Stripe). Check if there's an ITW Premium Stripe flow. If not, this trigger needs to be added to the ITW signup flow. - SermonWise Pro signup: Already handled in the CWA Stripe webhook at line ~260-279 (
session.metadata?.product === 'sermon_pro'). Addxpromo_sw_itwlogging here. - Same pattern as Kit Buyer: log the immediate email, let cron handle follow-ups.
Part 2: Deep QA Review
Internal QA (Agent Self-Review)
Run these checks AFTER wiring all Phase 3 triggers:
2A. Email Content QA
For EVERY email template in renderEmailHtml():
- Brand compliance — Sacred Gold (#D4AF37), Navy (#1B365D), Cream (#FEFCF8) used correctly
- CAN-SPAM compliance — marketing emails have unsubscribe link in footer
- Personalization —
${props.churchName},${props.pastorName}render correctly (not "undefined") - Links work — dashboard URL, chat page URL, pricing page URL all valid
- Plan-specific content — Starter/Pro/Suite emails show correct tool counts (25/35/39)
- Tone — warm, pastoral, not corporate. "We're rooting for your church" energy.
- Length — 3-4 paragraphs max. Pastors are busy.
2B. Dedup QA
- Manually insert a test row into
lifecycle_emails_sentfor the demo church - Run the cron — verify it does NOT re-send that email
- Call
sendAndLogdirectly with same church_id + email_key — verify it returns{ sent: false, skipped: 'already_sent' } - Delete the test row after verification
2C. Skip Condition QA
For the demo church (00000000-0000-4000-a000-000000000001):
- Add custom_hours to premium_churches → verify
day2_setup_starteris skipped - Add a FAQ to church_knowledge_base → verify
day2_setup_prois skipped - Add doctrinal_overrides → verify
day5_theology_prois skipped - Add a chatbot_conversation → verify
day7_activation_prois skipped - Remove each one → verify the email would now be sent
2D. Cron Safety QA
- Run cron twice in succession — verify 0 emails sent on second run (dedup)
- Verify
email_opt_out = trueon a test church → 0 emails sent - Verify cancelled church gets win-back but NOT onboarding emails
- Verify active church gets onboarding but NOT win-back emails
- Check Supabase 1000-row limit: if
premium_churchesgrows > 1000, the cron will silently stop processing extras. Add.range(0, 5000)or paginate.
2E. Stripe Webhook QA
- Create a test checkout session with
metadata.church_id= demo church - Verify welcome email is sent AND logged to
lifecycle_emails_sent - Verify starter kit email is sent AND logged
- Verify running cron after checkout does NOT re-send welcome/kit
External QA (Production Verification)
After deploying to Vercel:
2F. Resend Dashboard
- Check Resend dashboard for delivery confirmation of all test emails
- Verify no bounces or complaints
- Verify sender address is
hello@churchwiseai.com
2G. Live Cron Verification
- After Vercel deploy, check Vercel cron logs for
lifecycle-emailsroute - Verify it returns
{ processed: true, sent: 0, skipped: 0 }(no active churches to process yet) - If demo church has
activated_atset and status = 'active', some onboarding emails may fire — check Resend dashboard
2H. PewSearch Claim Flow
- Visit pewsearch.com/claim/[demo-slug]
- Verify the marketing opt-in checkbox is visible below the declaration
- Test with GDPR timezone (use browser dev tools to override timezone) — verify checkbox starts unchecked
- Complete a test claim → verify
marketing_opt_inis stored inpremium_churches
2I. Email Rendering
- Send a test email to a real inbox (founder's email)
- Check rendering in: Gmail web, Gmail mobile, Outlook, Apple Mail
- Verify: CTA buttons are clickable, images/gradients render, footer links work
- Verify: unsubscribe link goes to admin preferences page
Part 3: KB System Wiring
3A. Update manifest.yaml
The lifecycle email system and MailerLite changes need to be tracked in knowledge/manifest.yaml.
Add these entries:
- Under the
architecturesection, addlifecycle-email-system.mdwith its code-files:churchwiseai-web/src/lib/lifecycle-emails.tschurchwiseai-web/src/app/api/cron/lifecycle-emails/route.tschurchwiseai-web/src/app/api/stripe/webhook/route.tschurchwiseai-web/vercel.json
- Under the
integrationssection, verifymailerlite.mdhas its code-files listed:churchwiseai-web/src/lib/mailerlite.tschurchwiseai-web/src/lib/mailerlite-groups.tschurchwiseai-web/src/app/api/mailerlite/subscribe/route.tspewsearch/web/src/app/api/leads/capture/route.ts
3B. Update INDEX.md
Verify knowledge/INDEX.md includes:
architecture/lifecycle-email-system.mdin the Architecture sectionintegrations/mailerlite.mdin the Integrations section (should already be there)- Both docs should have correct descriptions matching their frontmatter
Run pnpm derive from C:\dev\knowledge\ to regenerate INDEX.md if needed.
3C. Cross-Reference Check
These docs reference each other — verify links are correct:
lifecycle-email-system.md→ should link tomailerlite.mdin "See Also"mailerlite.md→ should link tolifecycle-email-system.md(already added 2026-03-27)mailerlite.mdcode-files list → should includelifecycle-emails.ts(already updated)PRICING.md→ no changes needed (lifecycle emails reference pricing but don't modify it)
3D. Verify No Stale References
Check that no existing KB doc references MailerLite automations as the primary email system:
knowledge/narrative/customer-journey.md— may reference MailerLite automations for onboarding. Update to reference lifecycle system.knowledge/narrative/operations.md— may reference MailerLite for email ops. Update.knowledge/processes/— check for any email-related SOPs that reference MailerLite automations.
Search: grep -r "MailerLite.*automation\|automation.*MailerLite" knowledge/
3E. Decision Log
Append to C:\dev\DECISION_LOG.md:
## 2026-03-27 — Lifecycle Email System + Marketing Compliance
- Built code-based lifecycle email system replacing all 9 MailerLite automations
- 11 sequences, 25+ templates, daily cron, INSERT-before-send dedup
- MailerLite role reduced to: newsletter subscriber CRM + Care broadcasts only
- Added CAN-SPAM/GDPR marketing opt-in to PewSearch claim forms (ClaimForm + PastorLeadCapture)
- Phase 3 triggers (newsletter, course, kit, cross-promos) templates built but not yet wired
Files You'll Touch
Phase 3 Triggers (Part 1)
| File | What to do |
|---|---|
churchwiseai-web/src/app/api/cron/lifecycle-emails/route.ts | Add sections 4-6: newsletter subscribers, kit buyers, cross-promo triggers |
churchwiseai-web/src/app/api/stripe/webhook/route.ts | Add kit buyer + SermonWise Pro lifecycle logging |
churchwiseai-web/src/app/api/newsletter/route.ts | Fire immediate newsletter_welcome + course_day1, log to lifecycle_emails_sent |
pewsearch/web/src/app/api/leads/capture/route.ts | Log xpromo_ps_cwa_0 to lifecycle_emails_sent |
pewsearch/web/src/app/api/stripe/webhook/route.ts | Log xpromo_ps_cwa_0 for paid claims |
QA (Part 2)
| File | What to do |
|---|---|
churchwiseai-web/src/lib/lifecycle-emails.ts | Review all templates for brand, tone, links |
| Demo church data in Supabase | Set up test scenarios for skip conditions |
KB Wiring (Part 3)
| File | What to do |
|---|---|
knowledge/manifest.yaml | Add lifecycle-email-system.md entry with code-files |
knowledge/INDEX.md | Verify/regenerate via pnpm derive |
knowledge/narrative/customer-journey.md | Update MailerLite automation references |
knowledge/narrative/operations.md | Update email ops references |
C:\dev\DECISION_LOG.md | Append lifecycle email + compliance decision |
Testing Approach
- Use demo church (
churchwiseai-demo, UUID00000000-0000-4000-a000-000000000001) for all church-based tests - For non-church sequences, use a test email address (founder's email or
test@churchwiseai.com) - DO NOT write junk data to production — only use demo church + test subscribers
- Manually trigger cron via GET request with
Authorization: Bearer <CRON_SECRET>header - Check Resend dashboard after each test for delivery confirmation
- Verify
lifecycle_emails_senttable has correct entries after each test
Order of Operations
- Wire Phase 3 triggers (Part 1) — newsletter, course, kit nurture, cross-promos
- Build and deploy — verify no TypeScript errors, push to main/master
- Run internal QA (Part 2A-E) — dedup, skip conditions, cron safety, content
- Deploy and run external QA (Part 2F-I) — Resend dashboard, live cron, email rendering
- Wire KB (Part 3A-E) — manifest, INDEX, cross-references, stale references, decision log
- Disable MailerLite automations — one by one, ONLY after each replacement is verified working