Skip to main content

Pre-Conversation Journey — Complete Map

This document maps every step from "a pastor first hears about ChurchWiseAI" to "a visitor sends their first chatbot message or receives their first voice call answer." It is the definitive architecture reference for understanding how admin configuration data flows into the AI's live responses.


Architecture Overview: Two Parallel Tracks


Track A: Church Admin Journey (Setup)

Step A1: Discovery (Homepage and Product Pages)

What happens: A pastor finds churchwiseai.com via Google ad, PewSearch referral banner (?ref=pewsearch), word of mouth, or organic search. They land on the homepage or a product-specific page.

Code: churchwiseai-web/src/app/page.tsx (homepage), /voice/page.tsx, /chatbot/page.tsx

User sees:

  • Homepage: Hero video, two agent cards (Care Agent, Coordinator Agent), TheoLenses showcase, trust badges for 17 traditions, FAQ section ("Will this replace my church secretary?"), CTA to /pricing or /demo
  • Voice page: Feature list (24/7 answering, prayer request intake, visitor capture, call transcripts), ROI calculator, pricing tier comparison
  • Chatbot page: 39 ministry tools, hosted care pages, embed widget options

Data created/modified: None. Pure marketing content.

Config that flows downstream: Visitor's plan intent is captured in the URL when they click through to pricing (e.g., /pricing#voice).

Expected result: Pastor understands the product, is confident in theological alignment, and clicks "See Plans & Pricing" or "Try a Live Demo."

Failure mode: Stale or inaccurate marketing copy (agent counts, pricing, feature claims) causes trust failure. Agents must verify all marketing copy against product_knowledge table and PRICING.md before editing these pages.


Step A2: Pricing Page

What happens: Pastor reviews all tiers. Pricing is rendered by PricingGrid (client component). AgentShowcase shows all four agents with capability detail modals. FAQs cover common objections.

Code: churchwiseai-web/src/app/pricing/page.tsx, pricing/PricingGrid.tsx, pricing/AgentShowcase.tsx

User sees:

  • Tier cards: Starter Chat $14.95, Starter Voice $39.95, Starter Both $49.95, Pro Chat $34.95, Pro Voice $69.95, Pro Both $79.95, Suite Chat $59.95, Suite Both $99.95
  • "Founder Pricing — Lock in your rate for life" badge
  • FAQ: 14-day free trial for chat plans; voice/bundle plans have no trial but offer live demo numbers (US: 469-615-2221, Canada: 365-825-4095)
  • Ecosystem strip: PewSearch Premium ($9.95), IllustrateTheWord ($9.95), AI Starter Kit ($4.95 one-time)

Data created/modified: None.

Config that flows downstream: Plan key is passed as ?plan=X to the onboard form. Chat plans include 14-day trial; voice plans do not. Annual billing available for chat-only plans.

Expected result: Pastor selects a plan and clicks "Get Started" → redirects to /onboard?plan=starter_chat (or whichever tier).

Failure mode: If plan key in URL is invalid (isValidPlanKey() returns false), /onboard redirects back to /pricing rather than showing a broken form.


Step A3: Onboarding Form

What happens: Pastor fills out the onboarding form. This is a client-side React form (OnboardForm) that collects church details and stores them in sessionStorage — no database writes occur here. The form persists state to sessionStorage (key: onboard_form_v2) so back-button navigation restores fields.

Code: churchwiseai-web/src/app/onboard/page.tsx, onboard/components/OnboardForm.tsx

User sees:

  • Plan selector with billing toggle (monthly vs annual for chat plans)
  • Church Name, City, Country, State/Province (US and CA required)
  • Church Family dropdown (denomination families from DENOMINATION_FAMILIES) → Denomination cascade → denomination "other" text field
  • Contact Name, Email (required for crisis alerts), Phone (optional)
  • Marketing opt-in checkbox (pre-checked for US/CA; explicit opt-in for GDPR countries)
  • Optional backup contact (collapsible)
  • "Continue to Payment" button

Validation (client + server):

  • City required
  • State required for US and CA
  • Church family required ("Helps us calibrate the AI to your tradition")
  • Email required and valid format ("We use it to notify you immediately when someone needs urgent pastoral care")
  • Backup email must differ from primary email

On submit — what happens (no DB write yet):

  1. POST /api/onboard called with all form data
  2. Server checks rate limit (5/min), BotID (non-blocking), suspicion scoring (spam keywords, disposable email domains)
  3. Server tries to match church to PewSearch directory by name + city + state (SELECT only)
  4. Server checks if email already exists in premium_churches.admin_email — if yes, resends dashboard link and returns 409
  5. Server returns { ok: true, matchedChurchId: string|null }
  6. Client stores ALL form data + matchedChurchId in sessionStorage key cwa_onboard_data
  7. Client redirects to /onboard/checkout?tier=<plan>

Data created/modified:

  • sessionStorage['cwa_onboard_data'] — contains full form payload for checkout page
  • sessionStorage key onboard_form_v2 cleared after successful submit

Config that flows downstream:

  • denomination and churchFamily → stored in metadata → becomes denomination field in churches table → maps to theological lens
  • email → becomes admin_email → receives crisis alerts, welcome email, magic link
  • backupEmail → becomes backup identity with admin role
  • marketingOptIn → stored in premium_churches.marketing_opt_in → controls MailerLite sync

Expected result: Pastor moves to Stripe checkout with all their data preserved in session.

Failure mode: If sessionStorage is unavailable (private browsing modes), the form data cannot be passed to checkout. Checkout page shows "Session Expired" and prompts them to restart.


Step A4: Stripe Embedded Checkout

What happens: The checkout page reads sessionStorage['cwa_onboard_data'], calls /api/stripe/checkout-embedded (POST) with the full onboard data, which creates a Stripe Checkout session with all church data embedded in session.metadata. The Stripe Embedded Checkout renders in-page.

Code: churchwiseai-web/src/app/onboard/checkout/CheckoutContent.tsx, CheckoutForm.tsx

User sees:

  • Stripe's embedded payment form (card details, billing info)
  • Plan summary showing church name and selected tier

Key metadata stored in Stripe session:

metadata: {
church_name, contact_name, email, phone,
city, state, country, denomination, church_family,
matched_church_id, marketing_opt_in,
backup_name, backup_email, backup_phone,
tier
}

This metadata is the SOLE source of truth for provisioning — no DB row exists yet.

Data created/modified:

  • Stripe Checkout Session created (no Supabase writes yet)
  • 14-day trial added for chat-only plans (subscription_data.trial_period_days: 14)

Config that flows downstream: All form data is now embedded in Stripe metadata and survives even if the browser is closed.

Expected result: Pastor enters card details and clicks "Subscribe." Stripe processes payment and redirects to /onboard/return?session_id=<id>.

Failure mode: If the onboard data is missing from session storage (session expired), the checkout page shows a "Session Expired" warning with a link back to /onboard?plan=<tier>.


Step A5: Return Page (Polling for Webhook)

What happens: The return page polls /api/onboard/check-setup?session_id=<id> every 2 seconds (up to 15 polls = 30 seconds), waiting for the Stripe webhook to fire and provision the account.

Code: churchwiseai-web/src/app/onboard/return/ReturnContent.tsx

User sees:

  • Spinning "Setting Up Your Account..." indicator
  • On success: "Payment Successful! Redirecting to your dashboard..."
  • On timeout (30s): "Payment Successful! We're finishing setup — check your email shortly"

Expected result: Within a few seconds, the webhook fires and the return page detects the admin token, then redirects to /thank-you?email=X&token=<admin_token>.

Failure mode: If the webhook is delayed beyond 30 seconds, the church is told to check their email. The setup still completes when the webhook fires.


Step A6: Stripe Webhook — Full Account Provisioning

What happens: Stripe fires checkout.session.completed. The webhook handler detects metadata.church_name (new "payment-first" flow indicator) and calls provisionNewChurch(). This is the single point where everything is created.

Code: churchwiseai-web/src/app/api/stripe/webhook/route.ts (function provisionNewChurch)

Idempotency: The webhook checks premium_churches.stripe_subscription_id first — if already provisioned, it returns early. The webhook_events table also deduplicates by Stripe event ID.

What provisionNewChurch() creates (in order):

Step 1: Church record (churches table)

  • If matched_church_id in metadata matches a PewSearch directory entry → reuses that row, optionally updates denomination
  • Otherwise → creates new churches row with slug (up to 3 slug-collision retries), sets is_premium: false initially

Step 2: Premium record (premium_churches table, UPSERT)

  • Sets status: 'active', plan, channel, care_enabled: true
  • Stores admin_email, admin_name, admin_phone, marketing_opt_in
  • Stores Stripe IDs: stripe_customer_id, stripe_subscription_id
  • admin_token auto-generated by DB (UUID) — this is the magic link token

Step 3: Identities and sessions (church_admin_identities, auth_sessions)

  • Creates primary identity for admin email via createIdentity()
  • Adds admin role via church_identity_roles
  • Creates session via createSession()
  • If backup email provided → creates backup identity with same admin role

Step 4: Chatbot provisioning (provisionChatbot())

  • Checks organization_settings for existing row (idempotency)
  • Creates organization_settings row with default chatbot_config and agent_config based on plan tier
  • Updates premium_churches to set chatbot_enabled: true and chatbot_agent_id = church.id

Step 5: Admin team member (church_team_members table)

  • Creates first team member row with role admin if none exists

Step 6: Voice agent config (church_voice_agents table)

  • For ALL plans (not just voice): creates row with notification_email pre-populated from admin email
  • Sets prayer_requests_enabled: true, visitor_intake_enabled: true, callback_scheduling_enabled: true
  • Sets status: 'pending_setup'
  • For voice/both plans: also sends founder an email alert that a Telnyx number needs provisioning

Step 7: Welcome email

  • Sends magic link email to admin_email (retries up to 3 times)
  • Contains: church name, admin token URL (/admin/<token>), whether voice is included, trial end date

Data created/modified:

TableOperation
churchesINSERT (or UPDATE denomination on existing)
premium_churchesUPSERT — status=active, plan, channel, admin_token, Stripe IDs
church_admin_identitiesINSERT (primary + backup)
church_identity_rolesINSERT (admin role)
auth_sessionsINSERT
organization_settingsUPSERT — chatbot_config, agent_config
church_team_membersINSERT (admin member)
church_voice_agentsINSERT
webhook_eventsINSERT (idempotency log)
lifecycle_emails_sentINSERT (welcome email log)

Also fires: MailerLite subscriber sync (if marketing_opt_in), founder new-sale notification.

Config that flows downstream: organization_settings.agent_config (which agents are enabled per plan) and church_voice_agents.notification_email are immediately live. The church's AI is operational from this moment, using defaults.

Expected result: A fully provisioned church account exists. Admin has a working magic link. Default chatbot is live at /chat/<slug> and /care/<slug>.

Failure mode: If chatbot provisioning fails, a founder alert email is sent. The church still exists and has a subscription. Provisioning can be retried by running provisionChatbot(churchId, premiumId) manually.


Step A7: Welcome Email → Admin Dashboard Access

What happens: Admin clicks the magic link (/admin/<admin_token>) from their welcome email. The URL token resolves to their church account.

Code: churchwiseai-web/src/app/admin/[token]/page.tsx, src/lib/premium-queries.ts (resolveToken())

User sees: Full admin dashboard (Overview, Calls, Requests, Care, Training, Settings, Upgrade tabs — visibility gated by plan and role)

Data loaded on every page visit (server-side, parallel):

  • premium_churches record (plan, status, custom data)
  • churches record (name, slug, denomination)
  • church_voice_agents (phone number, status, pastor config)
  • Dashboard metrics (call counts, prayer requests, visitor contacts)
  • organization_settings (agent_config, agent_tool_config)
  • Team members, tool usage counts, agent conversation counts

Step A8: Training Tab — Teaching the AI About the Church

What happens: Admin clicks the "Training" tab and uses the seven sub-tabs to configure the AI's knowledge. This is the primary configuration surface.

Code: churchwiseai-web/src/app/admin/[token]/components/TrainingTab.tsx

Sub-tabs and what they configure:

Sub-tab: Church Knowledge

Component: ChurchKnowledgePanel Sections:

  • Hourspremium_churches.custom_hours (JSONB: { Sunday: ["9:00 AM", "11:00 AM"], ... })
  • Staff & Leadershippremium_churches.custom_staff (JSONB array: [{ name, title, bio, photo_url }])
  • Ministriespremium_churches.custom_ministries (JSONB array: [{ name, description }])
  • What to Expectpremium_churches.what_to_expect (JSONB: { dress_code, parking, children, first_visit, music_style, service_length })
  • Document Uploadunified_rag_content rows (type: document, with embedding) via /api/admin/kb-proxy/upload

API: PATCH /api/premium/update (for structured fields), /api/admin/kb-proxy/upload (for documents)

Sub-tab: This Week

Component: ThisWeekPanel Sections:

  • Sermon topic → church_voice_agents.sermon_topic
  • Sermon series → church_voice_agents.sermon_series
  • Theme verse → church_voice_agents.theme_verse
  • Weekly announcement → church_voice_agents.weekly_announcement

API: PATCH /api/premium/update

Sub-tab: FAQs

Component: FAQManagement Each FAQ creates a row in unified_rag_content:

INSERT INTO unified_rag_content (
organization_id, -- = church_voice_agents.id (chatbot_agent_id)
content_type, -- = 'faq'
title, -- = question text
content, -- = answer text
curation_status, -- = 'approved'
embedding, -- = vector generated from question+answer
metadata -- = { faq_category, visibility, exact_response }
)
  • exact_response: true → chatbot returns this answer verbatim without calling LLM (zero cost)
  • Content moderation checked before insertion
  • Plan limit: 50 FAQs for non-Suite plans

API: POST /api/admin/kb-proxy?action=faqs

Sub-tab: Theology

Component: TheologySettings

  • Select theological lens (17 traditions) → church_theological_lenses table (junction: church_id, theological_lens_id)
  • Optional doctrinal overrides → organization_settings.doctrinal_overrides (JSONB)

The lens selection is the single most impactful config choice. It gates:

  • Which doctrinal rules apply (theological_contradictions table filtered by lens_id)
  • Which lens vocabulary block is injected (sai_theological_lenses vocabulary)
  • What terminology the AI uses (e.g., "Holy Spirit" vs "Holy Ghost", "Mass" vs "service")

Sub-tab: Agents

Component: AgentTrainingPanel

  • Toggle Care Agent on/off → organization_settings.agent_config.care.enabled
  • Toggle Coordinator Agent on/off → organization_settings.agent_config.coordinator.enabled
  • Adjust personality (tone, warmth level) → organization_settings.agent_config.[type].personality
  • Set handoff rules (when to escalate to human) → organization_settings.agent_config.[type].handoff_rules
  • Pro/Suite plans also unlock Discipleship and Stewardship agents

API: PATCH /api/admin/agents

Sub-tab: Safety

Component: SafetyCompliance, ModerationDashboard

  • View moderation flags
  • Review crisis detection events
  • Compliance settings

Sub-tab: Simulator

Component: SimulatorPanel

  • Test the configured AI in a sandbox without affecting real visitors

Step A9: Settings Tab — Operational Configuration

What happens: Admin configures how the AI behaves operationally (notifications, voice number, integrations).

Code: churchwiseai-web/src/app/admin/[token]/components/SettingsTab.tsx

Key settings and where they flow:

SettingDB ColumnEffect
Notification emailchurch_voice_agents.notification_emailCrisis alert destination, prayer request notifications
Notification phonechurch_voice_agents.notification_phoneSMS alerts
Pastor namechurch_voice_agents.pastor_nameAI says "Let me connect you with [name]" on handoff
Voice persona (Cindy/Carson/Random)church_voice_agents.voice_idCartesia TTS voice used for voice agent
Welcome greetingchurch_voice_agents.welcome_greetingOpening line when voice call connects
Pastor availabilitychurch_voice_agents.pastor_availability_textAI tells callers when pastor is available for callbacks
Giving URLchurch_voice_agents.giving_urlLink AI shares when visitors ask about giving
Planning Center integrationchurch_voice_agents.pco_app_id, pco_enabledEnables calendar-based appointment booking
Cal.com integrationchurch_voice_agents.cal_event_type_idEnables callback scheduling via Cal.com
Custom church namepremium_churches.custom_nameDisplayed in chat UI and spoken by voice agent instead of directory name

Track B: Visitor Journey (Discovery → First Message)

Step B1: How Visitors Discover the Chat

PewSearch Discovery:

  • If a church has premium_churches.chatbot_enabled = true and status = 'active', PewSearch (/churches/[slug]) shows a floating "Chat with us" button
  • Code: pewsearch/web/src/app/churches/[slug]/page.tsx line ~454
  • Link target: https://churchwiseai.com/care/${church.slug} (opens in new tab)

Other discovery paths:

  • Church embeds the widget on their own website via <script src="...widget.js"> (configured in Settings tab → Widget Installer)
  • Church shares the Care Hub link in their bulletin, email newsletter, or SMS
  • Church posts a QR code that links to /care/<slug>
  • Visitor scans from a specific agent URL like /chat/<slug>?agent=care

Step B2: Care Hub — /care/[slug]

What happens: The Care Hub is the primary visitor entry point. It shows the church's name, photo, and cards for each enabled agent. Visitors can select which agent type to talk to or just ask a general question.

Code: churchwiseai-web/src/app/care/[slug]/page.tsx

Server-side data loaded:

  • churches — church name, photo, contact info
  • premium_churches — status, chatbot_enabled, custom_name, plan
  • organization_settings.agent_config — which agents are enabled (via resolveAgentConfig())
  • church_voice_agents.twilio_phone_number — shows "Call us" button if voice is provisioned
  • church_theological_lenses — if no lens set, shows lens selector UI (demo churches)

User sees:

  • Church name and photo hero header
  • Agent cards for each ENABLED agent type (Care, Coordinator, Discipleship, Stewardship)
  • Suggested demo prompts on each card ("Please pray for my friend who has cancer," "What time are services this Sunday?")
  • If voice number assigned: "Call us at (555) 123-4567" button
  • "Not sure which agent? Just ask a general question" fallback link to /chat/[slug]

Failure modes:

  • If premium_churches not found → notFound() (404)
  • If status not active or chatbot not enabled → graceful offline page showing church contact info and PewSearch listing link

Step B3: Chat Page — /chat/[slug]

What happens: The chat interface loads. It checks that the church is active and chatbot is enabled, then renders the ChatInterface client component.

Code: churchwiseai-web/src/app/chat/[slug]/page.tsx, ChatInterface.tsx

Server-side checks:

  • Loads churches record by slug
  • Loads premium_churches — if not found, redirects to /care/<slug> instead of 404
  • Checks status === 'active' (or valid preview) AND chatbot_enabled === true
  • If either fails → redirect to /care/<slug>

Props passed to ChatInterface:

  • churchName, churchCity, churchState, churchDenomination, churchSlug, churchId
  • showBranding — whether to show "Powered by ChurchWiseAI" badge (hidden on plans with remove_badge access)
  • upgradeUrl, upgradeMessage — for Pro Website basic chatbot tier

Expected result: Visitor sees the chat input and can send their first message.


Step B4: First Message — POST /api/chatbot/stream

What happens: Visitor types "I'd like to know more about your church" and clicks Send. The ChatInterface posts to /api/chatbot/stream. This is the most complex step — the full response assembly pipeline.

Code: churchwiseai-web/src/app/api/chatbot/stream/route.ts

Request payload:

{
"message": "visitor text",
"churchId": "uuid",
"sessionId": "browser-generated uuid",
"history": [...],
"agentType": "care|coordinator|discipleship|stewardship",
"lensOverride": null,
"lensNameOverride": null
}

End-to-End Data Flow: How Admin Config Reaches the Visitor

This is the key question: "If a church admin writes empathetic FAQ content in their Training tab, show me EXACTLY how that data flows into what the chatbot says to a visitor."

FAQ Content Flow

Admin Training Tab (FAQManagement component)

├─ Admin writes Q: "Do you have a nursery?" A: "Yes! Our Children's Ministry Director Sarah..."


POST /api/admin/kb-proxy?action=faqs

├─ Auth: resolveTokenOrHeaders() → verifies admin_token → confirms chatbot_agent_id
├─ Moderation: moderateText() checks content
├─ Embedding: generateEmbedding(question + answer) → 1536-dim vector


INSERT INTO unified_rag_content (
organization_id = church.id, -- chatbot_agent_id from premium_churches
content_type = 'faq',
title = "Do you have a nursery?",
content = "Yes! Our Children's Ministry Director Sarah...",
embedding = [0.023, -0.041, ...], -- vector for semantic search
metadata = { exact_response: false, faq_category: 'children', visibility: 'public' }
)


Visitor sends: "I'm nervous, do you have a nursery?"


POST /api/chatbot/stream

├─ FAQ short-circuit (BEFORE heavy context loading):
│ matchFAQ(message, churchId, agentType)
│ └─ If exact_response=true AND high similarity → return answer immediately (no LLM call)
│ └─ If fuzzy match → faqPreferredContext injected into LLM system prompt

├─ RAG retrieval (parallel):
│ searchChurchKnowledge(churchId, queryEmbedding)
│ └─ Calls RPC: search_church_knowledge(p_church_id, p_query_embedding, threshold=0.35)
│ └─ Returns FAQ row + any uploaded documents with similarity scores

├─ churchKnowledgeBlock assembled:
│ "[1] "Do you have a nursery?" (FAQ)
│ Yes! Our Children's Ministry Director Sarah..."

├─ Injected into LLM system prompt as:
│ "This is [Church Name]'s own knowledge base (FAQs and uploaded documents).
│ PRIORITIZE this content over general knowledge when answering questions about the church."


LLM response uses the empathetic FAQ answer in its reply to the visitor

Theology/Denomination Flow

Admin Training Tab → Theology sub-tab

├─ Admin selects "Reformed" (theological_lens_id = 4)


INSERT INTO church_theological_lenses (church_id, theological_lens_id = 4)


POST /api/chatbot/stream

├─ Resolve lens (priority chain):
│ 1. lensOverride from client (demo mode only) → skip if null
│ 2. SELECT from church_theological_lenses WHERE church_id = X → lens_id = 4, lens_name = "Reformed"
│ 3. DENOMINATION_TO_LENS[church.denomination] → fallback auto-detect
│ 4. Default: lens_id = 10 (Christocentric)

├─ Doctrinal rules (parallel with RAG):
│ SELECT FROM theological_contradictions WHERE lens_id = 4
│ → Returns rules like "BAPTISM: This church's position: Covenant/infant baptism.
│ MUST NEVER mention: believer's baptism, rebaptism"

├─ Lens vocabulary:
│ fetchLensVocabulary(lensId=4)
│ → Returns preferred terms: "Holy Scriptures" not "Bible", "congregation" not "audience"

├─ Both injected into system prompt as:
│ "IMPORTANT DOCTRINAL REQUIREMENTS: You MUST follow these theological positions..."
│ "[Vocabulary block for Reformed tradition]"


LLM responds to "What does your church believe about baptism?" using Reformed position

Staff Info Flow

Admin Training Tab → Church Knowledge → Staff section

├─ Admin adds: { name: "Pastor John Smith", title: "Lead Pastor", bio: "..." }


PATCH /api/premium/update


UPDATE premium_churches SET
custom_staff = '[{"name":"Pastor John Smith","title":"Lead Pastor","bio":"..."}]'
WHERE church_id = X


POST /api/chatbot/stream

├─ Load premium_churches:
│ SELECT custom_staff FROM premium_churches WHERE church_id = X
│ → staff = [{ name: "Pastor John Smith", title: "Lead Pastor", ... }]

├─ Structured fast path (Tier 0):
│ checkStructuredData(message, churchFacts)
│ → If visitor asks "Who is your pastor?", this may answer directly from staff array
│ without calling LLM

├─ Facts block (if LLM called):
│ "Staff & Leadership:
│ Pastor John Smith — Lead Pastor"
│ → Injected as literal text in system prompt


LLM has staff names and titles for any visitor question about leadership

Hours Flow

Admin Training Tab → Church Knowledge → Hours

├─ Admin sets: Sunday: ["9:00 AM", "11:00 AM"], Wednesday: ["7:00 PM"]


UPDATE premium_churches SET
custom_hours = '{"Sunday":["9:00 AM","11:00 AM"],"Wednesday":["7:00 PM"]}'
WHERE church_id = X


POST /api/chatbot/stream

├─ Structured fast path:
│ checkStructuredData("What time is the service?", churchFacts)
│ → Returns formatted hours string directly (no LLM needed)

├─ If LLM needed:
│ Facts block: "Hours:
│ Sunday: 9:00 AM, 11:00 AM
│ Wednesday: 7:00 PM"


Visitor asking "What time does church start?" gets instant structured answer

Agent Personality / Handoff Rules Flow

Admin Training Tab → Agents sub-tab

├─ Admin toggles Care Agent: enabled=true, personality={ warmth: 'high', tone: 'pastoral' }
├─ Admin sets handoff: escalate_when = "caller expresses crisis or requests pastor"


PATCH /api/admin/agents


UPDATE organization_settings SET
agent_config = '{
"care": { "enabled": true, "personality": {...}, "handoff_rules": {...} },
"coordinator": { "enabled": true, ... }
}'
WHERE organization_id = church.id


POST /api/chatbot/stream

├─ Check if requested agent is enabled:
│ effectiveAgentConfig = resolveAgentConfig(savedAgentConfig, planTier)
│ if (!effectiveAgentConfig[marketingAgent].enabled) → return "agent not active" message

├─ Build agent system prompt:
│ buildAgentSystemPrompt(agentType, enabledTools, personalityOverrides, handoffRules)
│ → Returns: "=== AGENT SPECIALIZATION: Care Agent ===
│ You are a specialized Care Agent...
│ [Domain ruleset for care]
│ [Personality: warm, pastoral tone]
│ [Handoff: escalate when crisis or pastor request]"

├─ Agent prompt appended to main system prompt


LLM behaves as configured Care Agent with custom warmth and handoff triggers

What-to-Expect Flow

Admin Training Tab → Church Knowledge → What to Expect

├─ Admin sets: dress_code="Casual — come as you are", parking="Free lot on Main Street",
│ children="Kids Ministry for ages 3-12", first_visit="Our greeters will..."


UPDATE premium_churches SET
what_to_expect = '{"dress_code":"Casual...","parking":"Free lot...","children":"Kids Ministry...","first_visit":"Our greeters..."}'


POST /api/chatbot/stream

├─ Structured fast path checks what_to_expect fields
├─ Facts block includes:
│ "What to Expect:
│ Dress code: Casual — come as you are
│ Parking: Free lot on Main Street
│ Children: Kids Ministry for ages 3-12
│ First visit: Our greeters will..."


Visitor asking "What should I wear?" or "Do you have something for my kids?"
gets church-specific answers from admin-entered content

Complete System Prompt Assembly (Full LLM Call)

When the full LLM path is needed (not FAQ short-circuit, not structured data, not semantic cache), the system prompt is assembled from these blocks in order:

1. BASE AGENT PERSONA (from agent-prompts.ts)
"You are a warm, caring AI assistant for [Church Name]..."
[Crisis detection rules — LIFE-SAFETY PROTECTED FILE]
[HEAR Protocol: Hear, Empathize, Advance, Respond]

2. AGENT SPECIALIZATION (from buildAgentSystemPrompt)
"=== AGENT SPECIALIZATION: Care Agent ==="
[Agent-specific system prompt + domain ruleset]
[Personality overrides from organization_settings.agent_config]
[Handoff rules]
[Tool usage instructions]

3. CHURCH FACTS (from premium_churches + churches + church_voice_agents)
Church: [Name]
Address: [address]
Denomination: [denomination]
Phone: [phone]
Hours: Sunday: 9:00 AM, 11:00 AM ...
Staff & Leadership: Pastor John — Lead Pastor
Ministries: Youth Group, Women's Bible Study...
What to Expect: Dress code: Casual...
Pastor Name: [pastor_name]
Pastor Availability: [pastor_availability_text]
Giving URL: [giving_url]
This Week's Sermon: [sermon_topic] / [sermon_series]
Theme Verse: [theme_verse]
Announcement: [weekly_announcement]

4. THEOLOGICAL LENS VOCABULARY (from sai_theological_lenses)
"Use these preferred terms for [Reformed] tradition:
Say 'congregation' not 'audience', 'Holy Scriptures' not 'Bible'..."

5. DOCTRINAL RULES (from theological_contradictions + org overrides)
"IMPORTANT DOCTRINAL REQUIREMENTS:
BAPTISM: This church's position is covenant/infant baptism.
MUST NEVER mention: believer's baptism, rebaptism..."

6. CURATED THEOLOGICAL CONTENT (from unified_rag_content, theological)
"--- Curated Knowledge Base ---
[1] "On Infant Baptism" (theological_content)
The Reformed tradition holds..."

7. CHURCH KNOWLEDGE BASE (from unified_rag_content, church-scoped FAQs + docs)
"--- Church Knowledge Base ---
[1] "Do you have a nursery?" (FAQ)
Yes! Our Children's Ministry Director Sarah..."

8. FAQ MATCH (if fuzzy match found, injected as preferred context)
"--- FAQ Match ---
[question/answer that partially matches visitor's query]"

9. PRODUCT KNOWLEDGE (from product_knowledge table, top 20 by priority)
"--- Product & Platform Knowledge ---
Q: How does ChurchWiseAI work?
A: [platform knowledge for chatbot to cite if asked]"

All 9 blocks are assembled before the LLM call. The LLM sees the full context window and generates a response that reflects the church's specific configuration.


Response Cascade (Cost Optimization Tiers)

Before reaching the full LLM call, the chatbot runs through a cascade of cheaper options:

TierWhat triggers itCostLatency
Tier 0: Structured dataVisitor asks about hours, address, staff, ministries — matches predefined patternsFree~0ms
Tier 1: Exact FAQ matchexact_response=true FAQ with high similarityFree~50ms
Tier 2: Semantic cacheSemantically similar question already answered for this churchFree~100ms
Tier 3: Direct retrievalSingle high-confidence RAG chunk (≥0.85 similarity) → fast Haiku formatting~$0.0001~500ms
Tier 4: Full LLMNone of the above triggered~$0.001~1-2s

This cascade means that well-configured churches (with FAQs marked exact_response=true) answer the most common questions for free, with zero LLM calls.


Voice Agent: Pre-Call Configuration (Parallel Track)

The voice agent loads configuration at call start, not via the chatbot API. Key differences:

Code: churchwiseai-web/voice-agent-livekit/session.py

On incoming call:

  1. Phone number looked up in PHONE_REGISTRY dict (hardcoded for known churches) OR queried from church_voice_agents.telnyx_phone_number / twilio_phone_number
  2. Full church config loaded from church_voice_agents and premium_churches
  3. load_product_knowledge() called — loads product_knowledge table entries
  4. All church facts assembled into voice agent system prompt (same data sources as chatbot: hours, staff, denomination, notification_email, sermon topic, etc.)
  5. Call begins with welcome_greeting from church_voice_agents.welcome_greeting

Voice-specific fields (not used by chatbot):

  • voice_id → Cartesia TTS voice selection (Cindy or Carson)
  • crisis_message, crisis_blessing → custom crisis response scripts
  • pastor_availability (JSON schedule) → when to offer callbacks
  • escalation_contact_name, escalation_when → smart call routing

Key Database Tables (Pre-Conversation Summary)

TableWhat it storesWho writes itWho reads it
churchesDirectory listing (name, slug, denomination, location)Stripe webhook (on sign up)Chat page, Care Hub, chatbot API
premium_churchesSubscription + all custom content (staff, hours, ministries, what_to_expect, custom_hours, custom_name)Stripe webhook, Admin dashboard PATCHChatbot API, Care Hub, admin page
church_voice_agentsVoice + operational config (pastor name, notification email, sermon topic, giving URL, Cal.com, PCO)Stripe webhook, Settings tab PATCHChatbot API, Voice agent (session.py)
organization_settingsAgent config (enabled agents, personalities, handoff rules), tool config, chatbot configprovisionChatbot(), Agents training tabChatbot API, Care Hub
unified_rag_contentFAQs (type=faq) + document chunks (type=document) per churchkb-proxy API (admin FAQ save, document upload)Chatbot API (searchChurchKnowledge RPC)
church_theological_lensesSelected theological lens ID for each churchTheology training tabChatbot API
theological_contradictionsDoctrinal rules per lens (must include/exclude terms)Seed data (not admin-editable)Chatbot API
sai_theological_lensesLens names, vocabulary, descriptions (17 traditions)Seed dataChatbot API, Care Hub
product_knowledgePlatform-level Q&A (pricing, features, how things work)Developer migrationsChatbot API, Voice agent

Critical Failure Points

FailureImpactDetection
Stripe webhook doesn't fireChurch pays but account never created. Admin has no magic link.premium_churches has no row for the Stripe subscription ID. Admin emails go to spam.
provisionChatbot() failsChurch exists in DB but chatbot shows "not enabled."organization_settings row missing. Founder alert email sent.
Welcome email fails 3 timesAdmin has no magic link.lifecycle_emails_sent row missing for welcome key. Admin can't access dashboard.
chatbot_agent_id null/api/admin/kb-proxy returns 400 "No chatbot configured."premium_churches.chatbot_agent_id is null after provisioning.
FAQ embedding not generatedFAQ exists but never appears in RAG results. Chatbot gives generic answers.unified_rag_content.embedding is null for the FAQ row.
No theological lens setChatbot defaults to Christocentric (lens 10). Wrong for Catholic, Orthodox, Reformed churches.church_theological_lenses table has no row for this church.
notification_email nullCrisis alerts, prayer request notifications go nowhere.church_voice_agents.notification_email is null.

Summary: The Journey in 10 Steps

  1. Pastor discovers churchwiseai.com (homepage, ads, PewSearch referral)
  2. Pastor evaluates plans on /pricing page, decides on a tier
  3. Pastor fills onboarding form at /onboard?plan=X — church info stored in sessionStorage
  4. Pastor pays via Stripe Embedded Checkout — all data stored in Stripe session metadata
  5. Stripe webhook fires — full account created: churches row, premium_churches, organization_settings (chatbot provisioned), church_voice_agents, identities, team member
  6. Welcome email sent with magic link → Pastor clicks → enters admin dashboard
  7. Pastor configures Training tab: hours, staff, ministries, what-to-expect, FAQs, theology lens, agent personalities
  8. Visitor discovers the church's care hub via PewSearch, bulletin, or QR code
  9. Visitor opens Care Hub (/care/<slug>) → selects agent → opens chat (/chat/<slug>)
  10. Visitor sends first message → chatbot API assembles response from ALL admin config: church facts, FAQ matches, theological lens, doctrinal rules, RAG retrieval, agent personality → LLM generates contextually appropriate, theologically grounded response