Skip to main content

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

MailerLiteOur System
Can't create automations via API100% API-controlled
Can only react to MailerLite data (tags, groups)Checks Supabase — did they set up FAQs? Service times?
Separate automation per planOne codebase, plan-aware
Templates in MailerLite UI (founder must edit manually)React Email in git — agents create/maintain
Stale copy goes unnoticedVersion-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

DayKeySubjectSkip If
0welcome_starter_chatWelcome to ChurchWiseAI, [Pastor]!— (webhook, immediate)
0starter_kit_starterYour AI Starter Kit is here— (webhook, immediate)
2day2_setup_starterQuick tip: add your service timeshas_hours
7day7_activation_starterHere's how churches are using their chatbothas_conversations
13day13_trial_last_starterYour trial ends tomorrow — here's what you'd lose

2. Onboarding — Pro Chat

DayKeySubjectSkip If
0welcome_pro_chatWelcome to ChurchWiseAI Pro, [Pastor]!— (webhook)
0starter_kit_proYour AI Starter Kit is here— (webhook)
2day2_setup_proQuick win: add your first FAQhas_faqs
5day5_theology_proMake your chatbot sound like your churchhas_theology
7day7_activation_pro35 tools your chatbot has — are you using them?has_conversations
13day13_trial_last_proYour Pro trial ends tomorrow — FAQs, analytics, embed gone
30day30_checkin_proYour first month — how's it going?

3. Onboarding — Suite Chat

DayKeySubjectSkip If
0welcome_suite_chatWelcome to ChurchWiseAI Suite, [Pastor]!— (webhook)
0starter_kit_suiteYour AI Starter Kit is here— (webhook)
2day2_setup_suiteQuick win: add your first FAQhas_faqs
5day5_theology_suiteMake your chatbot sound like your churchhas_theology
7day7_activation_suite39 tools, white-label ready — let's get you set uphas_conversations
13day13_trial_last_suiteYour Suite trial ends tomorrow
30day30_checkin_suiteYour first month — how's it going?

4. Newsletter Welcome (replaces MailerLite "CWA Newsletter Welcome Sequence")

DayKeySubjectTrigger
0newsletter_welcomeWelcome to ChurchWiseAI updatesNewsletter signup
2newsletter_why_aiWhy churches are adopting AI in 2026
5newsletter_demoSee a live chatbot in action

5. 7-Day AI Ministry Course (replaces MailerLite "7-Day AI Ministry Course")

DayKeySubjectTrigger
0course_day1Day 1: What AI can do for your churchNewsletter signup
1course_day2Day 2: Capturing prayer requests automatically
2course_day3Day 3: Never miss a visitor again
3course_day4Day 4: Your chatbot as a 24/7 church receptionist
4course_day5Day 5: Theological AI — respecting your tradition
5course_day6Day 6: What about sensitive conversations?
6course_day7Day 7: Getting started — your next step

6. Starter Kit Buyer Nurture (replaces MailerLite "CWA Starter Kit Buyer Nurture")

DayKeySubjectTrigger
0kit_deliveryYour AI Starter Kit is ready to downloadPurchase
3kit_followupDid you try the chatbot prompts?
7kit_upgradeReady for the real thing? Starter Chat from $14.95/mo

7. Cross-Promo: PewSearch → CWA (replaces MailerLite automation)

DayKeySubjectTrigger
0xpromo_ps_cwa_0Your listing is live — now add AI to your churchPewSearch claim
5xpromo_ps_cwa_5Churches with AI chatbots see 3x more engagement
14xpromo_ps_cwa_14Special offer: ChurchWiseAI at founder pricing

8. Cross-Promo: ITW → SermonWise (replaces MailerLite automation)

DayKeySubjectTrigger
3xpromo_itw_swLove illustrations? You'll love AI sermon prepITW Premium signup

9. Cross-Promo: SermonWise → ITW (replaces MailerLite automation)

DayKeySubjectTrigger
3xpromo_sw_itwNeed illustrations for your sermon? Meet IllustrateTheWordSermonWise signup

10. Cross-Promo: CWA → ShareWise (replaces MailerLite automation)

DayKeySubjectTrigger
7xpromo_cwa_shareYour church AI is running — now automate your social mediaCWA subscription active

11. Win-Back: Cancelled Customers (replaces MailerLite automation)

DayKeySubjectTrigger
3winback_3We miss you — here's what's new since you leftCancellation
14winback_14Your church data is still saved — come back anytime
30winback_30Special 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

  1. Build the system — table, cron, templates, Stripe webhook enhancements
  2. Test with demo church — send test sequences, verify skip conditions, verify dedup
  3. Disable MailerLite automations one by one — as each sequence is verified working
  4. MailerLite stays active for: newsletter subscriber management (forms, groups), Care tab broadcasts
  5. Monitor via WatchTower — add lifecycle email health check to daily audit

MailerLite Automations to Disable (9 total)

#AutomationStatusCreatedReplacement
1CWA Newsletter Welcome SequenceActive2025-10-17Sequence #4 (newsletter_welcome)
27-Day AI Ministry CourseActive2025-10-17Sequence #5 (course_day1-7)
3CWA Starter Kit Buyer NurtureActive2025-10-31Sequence #6 (kit_delivery/followup/upgrade)
4Cross-Promo: PewSearch → CWAActive2026-03-14Sequence #7 (xpromo_ps_cwa)
5Cross-Promo: ITW → SermonWiseActive2026-03-14Sequence #8 (xpromo_itw_sw)
6Cross-Promo: SermonWise → ITWActive2026-03-14Sequence #9 (xpromo_sw_itw)
7Cross-Promo: CWA → ShareWiseActive2026-03-14Sequence #10 (xpromo_cwa_share)
8Win-Back: Cancelled CustomersActive2026-03-14Sequence #11 (winback)
9CWA Trial NurturePAUSED/Incomplete2025-11-15Sequences #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 job
  • src/lib/lifecycle-emails.ts — sequence definitions, skip conditions, send logic
  • src/lib/email-templates/lifecycle/_layout.tsx — shared branded layout
  • src/lib/email-templates/lifecycle/*.tsx — ~25 email templates
  • pewsearch/migrations/xxx_lifecycle_emails_sent.sql — table creation

Modified files:

  • src/app/api/stripe/webhook/route.ts — enhance to send immediate lifecycle emails
  • src/lib/email.ts — may need to add Resend helpers
  • vercel.json — add cron schedule

Testing

  • Use demo church (churchwiseai-demo, UUID 00000000-0000-4000-a000-000000000001)
  • Create test entries in lifecycle_emails_sent to verify dedup
  • Manually trigger cron via GET /api/cron/lifecycle-emails with cron auth
  • Verify Resend delivery in Resend dashboard
  • Check WatchTower alerts for failures
  • Test skip conditions by toggling church data on/off