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
/pricingor/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):
POST /api/onboardcalled with all form data- Server checks rate limit (5/min), BotID (non-blocking), suspicion scoring (spam keywords, disposable email domains)
- Server tries to match church to PewSearch directory by name + city + state (SELECT only)
- Server checks if email already exists in
premium_churches.admin_email— if yes, resends dashboard link and returns 409 - Server returns
{ ok: true, matchedChurchId: string|null } - Client stores ALL form data +
matchedChurchIdinsessionStoragekeycwa_onboard_data - Client redirects to
/onboard/checkout?tier=<plan>
Data created/modified:
sessionStorage['cwa_onboard_data']— contains full form payload for checkout pagesessionStoragekeyonboard_form_v2cleared after successful submit
Config that flows downstream:
denominationandchurchFamily→ stored in metadata → becomesdenominationfield inchurchestable → maps to theological lensemail→ becomesadmin_email→ receives crisis alerts, welcome email, magic linkbackupEmail→ becomes backup identity with admin rolemarketingOptIn→ stored inpremium_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_idin metadata matches a PewSearch directory entry → reuses that row, optionally updatesdenomination - Otherwise → creates new
churchesrow with slug (up to 3 slug-collision retries), setsis_premium: falseinitially
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_tokenauto-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
adminrole viachurch_identity_roles - Creates session via
createSession() - If backup email provided → creates backup identity with same admin role
Step 4: Chatbot provisioning (provisionChatbot())
- Checks
organization_settingsfor existing row (idempotency) - Creates
organization_settingsrow with defaultchatbot_configandagent_configbased on plan tier - Updates
premium_churchesto setchatbot_enabled: trueandchatbot_agent_id = church.id
Step 5: Admin team member (church_team_members table)
- Creates first team member row with role
adminif none exists
Step 6: Voice agent config (church_voice_agents table)
- For ALL plans (not just voice): creates row with
notification_emailpre-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:
| Table | Operation |
|---|---|
churches | INSERT (or UPDATE denomination on existing) |
premium_churches | UPSERT — status=active, plan, channel, admin_token, Stripe IDs |
church_admin_identities | INSERT (primary + backup) |
church_identity_roles | INSERT (admin role) |
auth_sessions | INSERT |
organization_settings | UPSERT — chatbot_config, agent_config |
church_team_members | INSERT (admin member) |
church_voice_agents | INSERT |
webhook_events | INSERT (idempotency log) |
lifecycle_emails_sent | INSERT (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_churchesrecord (plan, status, custom data)churchesrecord (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:
- Hours →
premium_churches.custom_hours(JSONB:{ Sunday: ["9:00 AM", "11:00 AM"], ... }) - Staff & Leadership →
premium_churches.custom_staff(JSONB array:[{ name, title, bio, photo_url }]) - Ministries →
premium_churches.custom_ministries(JSONB array:[{ name, description }]) - What to Expect →
premium_churches.what_to_expect(JSONB:{ dress_code, parking, children, first_visit, music_style, service_length }) - Document Upload →
unified_rag_contentrows (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_lensestable (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_contradictionstable filtered bylens_id) - Which lens vocabulary block is injected (
sai_theological_lensesvocabulary) - 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:
| Setting | DB Column | Effect |
|---|---|---|
| Notification email | church_voice_agents.notification_email | Crisis alert destination, prayer request notifications |
| Notification phone | church_voice_agents.notification_phone | SMS alerts |
| Pastor name | church_voice_agents.pastor_name | AI says "Let me connect you with [name]" on handoff |
| Voice persona (Cindy/Carson/Random) | church_voice_agents.voice_id | Cartesia TTS voice used for voice agent |
| Welcome greeting | church_voice_agents.welcome_greeting | Opening line when voice call connects |
| Pastor availability | church_voice_agents.pastor_availability_text | AI tells callers when pastor is available for callbacks |
| Giving URL | church_voice_agents.giving_url | Link AI shares when visitors ask about giving |
| Planning Center integration | church_voice_agents.pco_app_id, pco_enabled | Enables calendar-based appointment booking |
| Cal.com integration | church_voice_agents.cal_event_type_id | Enables callback scheduling via Cal.com |
| Custom church name | premium_churches.custom_name | Displayed 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 = trueandstatus = 'active', PewSearch (/churches/[slug]) shows a floating "Chat with us" button - Code:
pewsearch/web/src/app/churches/[slug]/page.tsxline ~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 infopremium_churches— status, chatbot_enabled, custom_name, planorganization_settings.agent_config— which agents are enabled (viaresolveAgentConfig())church_voice_agents.twilio_phone_number— shows "Call us" button if voice is provisionedchurch_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_churchesnot 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
churchesrecord by slug - Loads
premium_churches— if not found, redirects to/care/<slug>instead of 404 - Checks
status === 'active'(or valid preview) ANDchatbot_enabled === true - If either fails → redirect to
/care/<slug>
Props passed to ChatInterface:
churchName,churchCity,churchState,churchDenomination,churchSlug,churchIdshowBranding— whether to show "Powered by ChurchWiseAI" badge (hidden on plans withremove_badgeaccess)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:
| Tier | What triggers it | Cost | Latency |
|---|---|---|---|
| Tier 0: Structured data | Visitor asks about hours, address, staff, ministries — matches predefined patterns | Free | ~0ms |
| Tier 1: Exact FAQ match | exact_response=true FAQ with high similarity | Free | ~50ms |
| Tier 2: Semantic cache | Semantically similar question already answered for this church | Free | ~100ms |
| Tier 3: Direct retrieval | Single high-confidence RAG chunk (≥0.85 similarity) → fast Haiku formatting | ~$0.0001 | ~500ms |
| Tier 4: Full LLM | None 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:
- Phone number looked up in
PHONE_REGISTRYdict (hardcoded for known churches) OR queried fromchurch_voice_agents.telnyx_phone_number / twilio_phone_number - Full church config loaded from
church_voice_agentsandpremium_churches load_product_knowledge()called — loadsproduct_knowledgetable entries- All church facts assembled into voice agent system prompt (same data sources as chatbot: hours, staff, denomination, notification_email, sermon topic, etc.)
- Call begins with
welcome_greetingfromchurch_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 scriptspastor_availability(JSON schedule) → when to offer callbacksescalation_contact_name,escalation_when→ smart call routing
Key Database Tables (Pre-Conversation Summary)
| Table | What it stores | Who writes it | Who reads it |
|---|---|---|---|
churches | Directory listing (name, slug, denomination, location) | Stripe webhook (on sign up) | Chat page, Care Hub, chatbot API |
premium_churches | Subscription + all custom content (staff, hours, ministries, what_to_expect, custom_hours, custom_name) | Stripe webhook, Admin dashboard PATCH | Chatbot API, Care Hub, admin page |
church_voice_agents | Voice + operational config (pastor name, notification email, sermon topic, giving URL, Cal.com, PCO) | Stripe webhook, Settings tab PATCH | Chatbot API, Voice agent (session.py) |
organization_settings | Agent config (enabled agents, personalities, handoff rules), tool config, chatbot config | provisionChatbot(), Agents training tab | Chatbot API, Care Hub |
unified_rag_content | FAQs (type=faq) + document chunks (type=document) per church | kb-proxy API (admin FAQ save, document upload) | Chatbot API (searchChurchKnowledge RPC) |
church_theological_lenses | Selected theological lens ID for each church | Theology training tab | Chatbot API |
theological_contradictions | Doctrinal rules per lens (must include/exclude terms) | Seed data (not admin-editable) | Chatbot API |
sai_theological_lenses | Lens names, vocabulary, descriptions (17 traditions) | Seed data | Chatbot API, Care Hub |
product_knowledge | Platform-level Q&A (pricing, features, how things work) | Developer migrations | Chatbot API, Voice agent |
Critical Failure Points
| Failure | Impact | Detection |
|---|---|---|
| Stripe webhook doesn't fire | Church 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() fails | Church exists in DB but chatbot shows "not enabled." | organization_settings row missing. Founder alert email sent. |
| Welcome email fails 3 times | Admin 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 generated | FAQ exists but never appears in RAG results. Chatbot gives generic answers. | unified_rag_content.embedding is null for the FAQ row. |
| No theological lens set | Chatbot defaults to Christocentric (lens 10). Wrong for Catholic, Orthodox, Reformed churches. | church_theological_lenses table has no row for this church. |
notification_email null | Crisis alerts, prayer request notifications go nowhere. | church_voice_agents.notification_email is null. |
Summary: The Journey in 10 Steps
- Pastor discovers churchwiseai.com (homepage, ads, PewSearch referral)
- Pastor evaluates plans on
/pricingpage, decides on a tier - Pastor fills onboarding form at
/onboard?plan=X— church info stored in sessionStorage - Pastor pays via Stripe Embedded Checkout — all data stored in Stripe session metadata
- Stripe webhook fires — full account created: churches row, premium_churches, organization_settings (chatbot provisioned), church_voice_agents, identities, team member
- Welcome email sent with magic link → Pastor clicks → enters admin dashboard
- Pastor configures Training tab: hours, staff, ministries, what-to-expect, FAQs, theology lens, agent personalities
- Visitor discovers the church's care hub via PewSearch, bulletin, or QR code
- Visitor opens Care Hub (
/care/<slug>) → selects agent → opens chat (/chat/<slug>) - 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