Lifecycle Email System
Problem
We have 8 MailerLite automations sending stale/inaccurate info. MailerLite's API cannot create or modify automations programmatically — only the UI can. The founder hates the MailerLite UI. Agents can't maintain these automations. Result: emails rot.
Email Trigger Points
Solution
Replace ALL MailerLite automations with a code-based system:
- Resend for sending (already integrated)
- Vercel cron for scheduling (already have cron infrastructure)
- Supabase table for sent-log / deduplication
- React Email for templates (already the pattern for welcome email)
- MailerLite stays for newsletter subscriber management and Care broadcasts ONLY
Why Not MailerLite
| MailerLite | Our System |
|---|---|
| Can't create automations via API | 100% API-controlled |
| Can only react to MailerLite data (tags, groups) | Checks Supabase — did they set up FAQs? Service times? |
| Separate automation per plan | One codebase, plan-aware |
| Templates in MailerLite UI (founder must edit manually) | React Email in git — agents create/maintain |
| Stale copy goes unnoticed | Version-controlled, auditable |
| A/B testing built-in (but unused) | Can add later if needed |
Architecture
Stripe Webhook (immediate) Daily Cron, 8am ET (scheduled)
│ │
▼ ▼
Welcome email Query premium_churches
AI Starter Kit WHERE status IN ('active','preview')
Payment failed AND activated_at IS NOT NULL
Cancellation │
Trial will end (day 11) ▼
│ For each church:
▼ compute days_since_activation
Send via Resend check SCHEDULE[plan]
Log to lifecycle_emails_sent check lifecycle_emails_sent (already sent?)
check skip conditions (Supabase)
│
▼
Send via Resend
Log to lifecycle_emails_sent
Database
CREATE TABLE lifecycle_emails_sent (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
church_id UUID NOT NULL,
email TEXT NOT NULL, -- recipient email address
email_key TEXT NOT NULL, -- 'welcome_pro_chat', 'day2_nudge_pro', etc.
sequence TEXT NOT NULL, -- 'onboarding', 'winback', 'crosspromo_ps_cwa', etc.
plan TEXT, -- plan at time of send (may change later)
sent_at TIMESTAMPTZ NOT NULL DEFAULT now(),
resend_message_id TEXT, -- for delivery tracking
UNIQUE(church_id, email_key) -- PREVENTS DOUBLE-SENDS — this IS the state machine
);
CREATE INDEX idx_lifecycle_church ON lifecycle_emails_sent(church_id);
CREATE INDEX idx_lifecycle_sequence ON lifecycle_emails_sent(sequence);
The UNIQUE constraint on (church_id, email_key) is the entire state machine. No sequence table. No step tracking. The cron is idempotent — it can run 100 times safely.
Sequences
1. Onboarding — Starter Chat
| Day | Key | Subject | Skip If |
|---|---|---|---|
| 0 | welcome_starter_chat | Welcome to ChurchWiseAI, [Pastor]! | — (webhook, immediate) |
| 0 | starter_kit_starter | Your AI Starter Kit is here | — (webhook, immediate) |
| 2 | day2_setup_starter | Quick tip: add your service times | has_hours |
| 7 | day7_activation_starter | Here's how churches are using their chatbot | has_conversations |
| 13 | day13_trial_last_starter | Your trial ends tomorrow — here's what you'd lose | — |
2. Onboarding — Pro Chat
| Day | Key | Subject | Skip If |
|---|---|---|---|
| 0 | welcome_pro_chat | Welcome to ChurchWiseAI Pro, [Pastor]! | — (webhook) |
| 0 | starter_kit_pro | Your AI Starter Kit is here | — (webhook) |
| 2 | day2_setup_pro | Quick win: add your first FAQ | has_faqs |
| 5 | day5_theology_pro | Make your chatbot sound like your church | has_theology |
| 7 | day7_activation_pro | 35 tools your chatbot has — are you using them? | has_conversations |
| 13 | day13_trial_last_pro | Your Pro trial ends tomorrow — FAQs, analytics, embed gone | — |
| 30 | day30_checkin_pro | Your first month — how's it going? | — |
3. Onboarding — Suite Chat
| Day | Key | Subject | Skip If |
|---|---|---|---|
| 0 | welcome_suite_chat | Welcome to ChurchWiseAI Suite, [Pastor]! | — (webhook) |
| 0 | starter_kit_suite | Your AI Starter Kit is here | — (webhook) |
| 2 | day2_setup_suite | Quick win: add your first FAQ | has_faqs |
| 5 | day5_theology_suite | Make your chatbot sound like your church | has_theology |
| 7 | day7_activation_suite | 39 tools, white-label ready — let's get you set up | has_conversations |
| 13 | day13_trial_last_suite | Your Suite trial ends tomorrow | — |
| 30 | day30_checkin_suite | Your first month — how's it going? | — |
4. Newsletter Welcome (replaces MailerLite "CWA Newsletter Welcome Sequence")
| Day | Key | Subject | Trigger |
|---|---|---|---|
| 0 | newsletter_welcome | Welcome to ChurchWiseAI updates | Newsletter signup |
| 2 | newsletter_why_ai | Why churches are adopting AI in 2026 | — |
| 5 | newsletter_demo | See a live chatbot in action | — |
5. 7-Day AI Ministry Course (replaces MailerLite "7-Day AI Ministry Course")
| Day | Key | Subject | Trigger |
|---|---|---|---|
| 0 | course_day1 | Day 1: What AI can do for your church | Newsletter signup |
| 1 | course_day2 | Day 2: Capturing prayer requests automatically | — |
| 2 | course_day3 | Day 3: Never miss a visitor again | — |
| 3 | course_day4 | Day 4: Your chatbot as a 24/7 church receptionist | — |
| 4 | course_day5 | Day 5: Theological AI — respecting your tradition | — |
| 5 | course_day6 | Day 6: What about sensitive conversations? | — |
| 6 | course_day7 | Day 7: Getting started — your next step | — |
6. Starter Kit Buyer Nurture (replaces MailerLite "CWA Starter Kit Buyer Nurture")
| Day | Key | Subject | Trigger |
|---|---|---|---|
| 0 | kit_delivery | Your AI Starter Kit is ready to download | Purchase |
| 3 | kit_followup | Did you try the chatbot prompts? | — |
| 7 | kit_upgrade | Ready for the real thing? Starter Chat from $14.95/mo | — |
7. Cross-Promo: PewSearch → CWA (replaces MailerLite automation)
| Day | Key | Subject | Trigger |
|---|---|---|---|
| 0 | xpromo_ps_cwa_0 | Your listing is live — now add AI to your church | PewSearch claim |
| 5 | xpromo_ps_cwa_5 | Churches with AI chatbots see 3x more engagement | — |
| 14 | xpromo_ps_cwa_14 | Special offer: ChurchWiseAI at founder pricing | — |
8. Cross-Promo: ITW → SermonWise (replaces MailerLite automation)
| Day | Key | Subject | Trigger |
|---|---|---|---|
| 3 | xpromo_itw_sw | Love illustrations? You'll love AI sermon prep | ITW Premium signup |
9. Cross-Promo: SermonWise → ITW (replaces MailerLite automation)
| Day | Key | Subject | Trigger |
|---|---|---|---|
| 3 | xpromo_sw_itw | Need illustrations for your sermon? Meet IllustrateTheWord | SermonWise signup |
10. Cross-Promo: CWA → ShareWise (replaces MailerLite automation)
| Day | Key | Subject | Trigger |
|---|---|---|---|
| 7 | xpromo_cwa_share | Your church AI is running — now automate your social media | CWA subscription active |
11. Win-Back: Cancelled Customers (replaces MailerLite automation)
| Day | Key | Subject | Trigger |
|---|---|---|---|
| 3 | winback_3 | We miss you — here's what's new since you left | Cancellation |
| 14 | winback_14 | Your church data is still saved — come back anytime | — |
| 30 | winback_30 | Special offer: come back at 50% off for 3 months | — |
Skip Conditions
const SKIP_CONDITIONS: Record<string, (churchId: string) => Promise<boolean>> = {
has_hours: async (id) => {
const { data } = await supabase.from('premium_churches')
.select('custom_hours').eq('church_id', id).single();
return data?.custom_hours && Object.keys(data.custom_hours).length > 0;
},
has_faqs: async (id) => {
const { count } = await supabase.from('church_knowledge_base')
.select('*', { count: 'exact', head: true })
.eq('church_id', id).eq('type', 'faq');
return (count ?? 0) > 0;
},
has_theology: async (id) => {
const { data } = await supabase.from('organization_settings')
.select('theological_lens').eq('church_id', id).single();
return !!data?.theological_lens;
},
has_conversations: async (id) => {
const { count } = await supabase.from('chatbot_conversations')
.select('*', { count: 'exact', head: true })
.eq('organization_id', id);
return (count ?? 0) > 0;
},
has_embed: async (id) => {
// Check if embed code was accessed/copied — may need a tracking event
return false; // TODO: implement when tracking exists
},
};
Templates
React Email components at src/lib/email-templates/lifecycle/:
lifecycle/
_layout.tsx — shared branded layout (Sacred Gold, Navy, Cream)
welcome-starter-chat.tsx
welcome-pro-chat.tsx
welcome-suite-chat.tsx
starter-kit.tsx
setup-nudge.tsx — plan-aware (different tips per plan)
theology-nudge.tsx
activation-nudge.tsx — plan-aware (different tool counts)
trial-last-day.tsx — plan-aware (different features lost)
first-month-checkin.tsx
payment-failed.tsx
cancellation.tsx
newsletter-welcome.tsx
course-day-[1-7].tsx
kit-buyer-nurture.tsx
crosspromo-ps-cwa.tsx
crosspromo-itw-sw.tsx
crosspromo-sw-itw.tsx
crosspromo-cwa-share.tsx
winback.tsx — day-aware (different messaging per step)
MailerLite Template Settings (use these when sending via MailerLite campaigns API)
If we send any emails through MailerLite (e.g., Care broadcasts), use these settings:
- Automatic preheader: ON — adds preheader with web version link
- Automatic CSS inline: ON — some email clients strip
<style>blocks - Automatic footer: ON — adds CAN-SPAM compliant footer with unsubscribe
- Preferences link: INCLUDE — let subscribers update their preferences
For Resend-sent lifecycle emails, we must handle these ourselves:
- Preheader: Include as hidden text at top of HTML template
- CSS inlining: Use React Email's built-in inlining (already handles this)
- Unsubscribe: Include unsubscribe link in footer for marketing emails (nudges, cross-promos, win-back). Transactional emails (welcome, payment failed, trial warning) don't legally require it but should include a preferences link.
- Preferences link: Include in all emails — links to
/admin/[token]?tab=settings&sub=notifications
Each template receives typed props:
type LifecycleEmailProps = {
churchName: string;
pastorName: string;
plan: string;
dashboardUrl: string;
chatPageUrl: string;
trialEndsAt?: string;
featuresAtRisk?: string[]; // for trial-ending emails
daysSinceSignup?: number; // for nurture context
};
Cron Job
New file: src/app/api/cron/lifecycle-emails/route.ts
// Pseudocode
export async function GET(req: Request) {
verifyVercelCron(req);
// 1. Get all active/preview churches with activation date
const churches = await getActiveChurches();
// 2. Get all emails already sent
const sentEmails = await getSentEmails(churches.map(c => c.church_id));
// 3. For each church, check what's due
for (const church of churches) {
const daysSinceActivation = diffDays(now, church.activated_at);
const schedule = SCHEDULE[church.plan] || SCHEDULE.starter_chat;
for (const email of schedule) {
if (daysSinceActivation < email.delay_days) continue;
if (sentEmails.has(`${church.church_id}:${email.key}`)) continue;
if (email.skipIf && await SKIP_CONDITIONS[email.skipIf](church.church_id)) continue;
await sendAndLog(church, email);
}
}
// 4. Also check non-church sequences (newsletter, course, kit buyers)
await processNewsletterSequences();
await processCourseSequences();
await processKitBuyerSequences();
// 5. Report to WatchTower
return NextResponse.json({ processed: churches.length, sent: sentCount });
}
Vercel cron config in vercel.json:
{
"crons": [
{ "path": "/api/cron/lifecycle-emails", "schedule": "0 13 * * *" }
]
}
(13:00 UTC = 8:00 AM ET)
Migration Plan
- Build the system — table, cron, templates, Stripe webhook enhancements
- Test with demo church — send test sequences, verify skip conditions, verify dedup
- Disable MailerLite automations one by one — as each sequence is verified working
- MailerLite stays active for: newsletter subscriber management (forms, groups), Care tab broadcasts
- Monitor via WatchTower — add lifecycle email health check to daily audit
MailerLite Automations to Disable (9 total)
| # | Automation | Status | Created | Replacement |
|---|---|---|---|---|
| 1 | CWA Newsletter Welcome Sequence | Active | 2025-10-17 | Sequence #4 (newsletter_welcome) |
| 2 | 7-Day AI Ministry Course | Active | 2025-10-17 | Sequence #5 (course_day1-7) |
| 3 | CWA Starter Kit Buyer Nurture | Active | 2025-10-31 | Sequence #6 (kit_delivery/followup/upgrade) |
| 4 | Cross-Promo: PewSearch → CWA | Active | 2026-03-14 | Sequence #7 (xpromo_ps_cwa) |
| 5 | Cross-Promo: ITW → SermonWise | Active | 2026-03-14 | Sequence #8 (xpromo_itw_sw) |
| 6 | Cross-Promo: SermonWise → ITW | Active | 2026-03-14 | Sequence #9 (xpromo_sw_itw) |
| 7 | Cross-Promo: CWA → ShareWise | Active | 2026-03-14 | Sequence #10 (xpromo_cwa_share) |
| 8 | Win-Back: Cancelled Customers | Active | 2026-03-14 | Sequence #11 (winback) |
| 9 | CWA Trial Nurture | PAUSED/Incomplete | 2025-11-15 | Sequences #1-3 (onboarding per plan) — this is exactly what we're building, but per-plan and with skip conditions |
Note: The CWA Trial Nurture was started Nov 2025 but never completed. It was the original attempt at what our lifecycle system now does properly — plan-specific onboarding with smart skip conditions. Delete it from MailerLite after our system is live.
Files to Create/Modify
New files:
src/app/api/cron/lifecycle-emails/route.ts— the cron jobsrc/lib/lifecycle-emails.ts— sequence definitions, skip conditions, send logicsrc/lib/email-templates/lifecycle/_layout.tsx— shared branded layoutsrc/lib/email-templates/lifecycle/*.tsx— ~25 email templatespewsearch/migrations/xxx_lifecycle_emails_sent.sql— table creation
Modified files:
src/app/api/stripe/webhook/route.ts— enhance to send immediate lifecycle emailssrc/lib/email.ts— may need to add Resend helpersvercel.json— add cron schedule
Testing
- Use demo church (
churchwiseai-demo, UUID00000000-0000-4000-a000-000000000001) - Create test entries in
lifecycle_emails_sentto verify dedup - Manually trigger cron via
GET /api/cron/lifecycle-emailswith cron auth - Verify Resend delivery in Resend dashboard
- Check WatchTower alerts for failures
- Test skip conditions by toggling church data on/off