Knowledge > Runbooks > Business Ops > Launch a New Vertical
Launch a New Vertical
End-to-end playbook for launching a new WiseAI Agency vertical (funeral home, vet clinic, law firm, auto shop, etc.). All verticals share one churchwiseai-web codebase, one Supabase instance, and one Stripe account — differentiated by hostname rewrite, per-vertical tenant tables, and a vertical column on shared-core tables.
This runbook is the canonical playbook. It was last validated by the FuneralWiseAI launch on 2026-04-21/22. Follow it in order. Every numbered step has been done in production at least once.
0. Prerequisites
- Founder has approved the vertical + launch timeline (per
narrative/strategy.mdvertical expansion sequencing) - Domain is acquired at Porkbun (
funeralwiseai.com,veterinarywiseai.com,legalwiseai.com,shopwiseai.com— all held as of 2026-04-22) - Vertical is added to
C:/dev/FEATURE_REGISTRY.md - Budget confirmed for new Stripe products + Telnyx number (if voice demo needed) + LiveKit secrets
- Read the architecture decision doc:
decisions/2026-04-21-multi-vertical-tenant-architecture.md
Canonical plan-key convention
Plan keys are globally unique across verticals:
| Vertical | Prefix | Example keys |
|---|---|---|
| ChurchWiseAI | cwa_ | cwa_starter_chat, cwa_pro_website, cwa_demo_prospect |
| FuneralWiseAI | fwa_ | fwa_starter, fwa_demo_prospect |
| VetWiseAI | vwa_ | (planned) |
| LegalWiseAI | lwa_ | (planned) |
| ShopWiseAI | swa_ | (planned) |
Enforced via CHECK constraint on each per-vertical premium_* table (plan ~ '^fwa_' etc.). Accidental church-plan writes into a funeral table are DB-level rejected.
Phase 1: Per-vertical tables (DDL migration)
Every vertical needs three new tables. Name pattern: <vertical>s, premium_<vertical>s, <vertical>_knowledge_base.
Canonical example (funeral): see pewsearch/migrations/20260421_multi_vertical_migration_1_funeral_tables.sql.
Step 1.1 — Create the 3 tables
For VetWiseAI, write migration <yyyymmdd>_vet_tenant_tables.sql following the funeral pattern:
<vertical>s— tenant identity (no public directory equivalent — these tables hold customers + demo prospects only, not a 218K-row scraped listing likechurches)premium_<vertical>s— subscription + admin state.plancolumn with CHECK constraintplan ~ '^<prefix>_'.admin_tokenUNIQUE. Cascade delete from<vertical>s.<vertical>_knowledge_base— FAQ editor source. Mirror ofchurch_knowledge_base.curation_statusenum. Cascade delete from<vertical>s.
Step 1.2 — Apply via Supabase MCP
mcp__plugin_supabase_supabase__apply_migration({
name: "<vertical>_tenant_tables",
query: "<full DDL>",
})
Step 1.3 — Verify
Query information_schema.columns to confirm all 3 tables exist with expected columns, triggers, CHECK constraints, and indexes. Migration 1 DDL is safe (additive, no existing-data impact).
Reference spec: architecture/multi-vertical-schema.md has the full DDL template.
Phase 2: Shared-core table is already multi-vertical (no changes needed for verticals in the existing allowlist)
As of 2026-04-21, 12 shared-core tables have a vertical column + CHECK allowing ('church','funeral','veterinary','legal','shop'):
tenant_voice_agents(waschurch_voice_agents)tenant_team_members(waschurch_team_members)voice_urgent_requests(wasvoice_prayer_requests)voice_visitor_inquiries(wasvoice_visitor_contacts)voice_callback_requests,voice_call_logs,tool_invocationsproduct_knowledge,chatbot_messages,chatbot_conversations,chatbot_questions_log,organization_settings
Adding a brand-new vertical not in the allowlist (e.g., dental, restaurant) requires one DO $$ block extending the CHECK — see 20260421_multi_vertical_migration_2_shared_core.sql lines 34-56 for the pattern.
Gotcha from M2 rollout (don't repeat): the original M2 renamed column church_id → tenant_id on 3 tables (voice_call_logs, voice_callback_requests, tool_invocations) that had NO backward-compat views. Every code caller using .eq('church_id', ...) broke. Took down live voice agents for ~1 hour. M2c reverted those column renames. See feedback memory feedback_never_migrate_before_audit.md. The 4 tables that WERE renamed have backward-compat views that alias tenant_id → church_id.
Phase 3: DNS + Vercel domain
Step 3.1 — Point the domain to Vercel at Porkbun
- Log in to Porkbun → DNS settings
- Add
Arecord:76.76.21.21(Vercel) - Add
CNAME www:cname.vercel-dns.com
Step 3.2 — Add the domain to Vercel churchwiseai-web project
vercel domains add <vertical>wiseai.com
If the domain is currently attached to another project, first remove it via the Vercel REST API (DELETE /v9/projects/:id/domains/:domain). The CLI only supports "add" and "remove from team."
Step 3.3 — Verify SSL cert provisions within minutes
Phase 4: Hostname rewrite in middleware
Add the hostname rewrite following the existing pattern (sermonwise.ai, sharewiseai.com, funeralwiseai.com, wiseaiagency.com).
File: churchwiseai-web/src/middleware.ts
if (hostname === '<vertical>wiseai.com' || hostname === 'www.<vertical>wiseai.com') {
return NextResponse.rewrite(new URL(`/<vertical>${pathname}`, request.url));
}
Phase 5: Marketing pages
Scaffold under src/app/<vertical>/:
| Route | Purpose | Reference |
|---|---|---|
/<vertical> | Home — hero, value-prop trio, how-it-works, pricing callout, founder story, CTA | src/app/funeralwiseai/page.tsx |
/<vertical>/how-it-works | HEAR protocol adapted for this vertical | src/app/funeralwiseai/how-it-works/page.tsx |
/<vertical>/pricing | Pricing + FAQ + guarantee | src/app/funeralwiseai/pricing/page.tsx |
/<vertical>/about | Founder credentials, domain-appropriate positioning | src/app/funeralwiseai/about/page.tsx |
/<vertical>/book | Demo booking form (POST to /api/contact with source=<vertical>) | src/app/funeralwiseai/book/page.tsx |
Create layout at src/app/<vertical>/layout.tsx with data-brand="<vertical>" so CSS tokens swap to the vertical's palette.
Define brand tokens at src/styles/tokens/brands/<vertical>.css — palette, typography, accent.
Update churchwiseai-web/CLAUDE.md Pages table with all new routes.
Phase 6: Stripe products
Confirm with founder before creating in live mode (per CLAUDE.md rule 10).
Test mode first
stripe products create --name "VetWiseAI Starter"
stripe prices create --product prod_xxx --unit-amount 19900 --currency usd --recurring[interval]=month
Live mode (after founder approval)
stripe products create --name "VetWiseAI Starter" --api-key $STRIPE_LIVE_SECRET_KEY
Add price IDs to:
C:/dev/PRICING.mdknowledge/data/pricing.yamlunder a newverticals:entry- Run
pnpm deriveinknowledge/to propagate
Add env vars to Vercel (all 3 environments)
printf "price_xxx" | vercel env add STRIPE_PRICE_<VERTICAL>_STARTER_MONTHLY production
printf "price_xxx" | vercel env add STRIPE_PRICE_<VERTICAL>_STARTER_MONTHLY preview
printf "price_xxx" | vercel env add STRIPE_PRICE_<VERTICAL>_STARTER_MONTHLY development
Use printf not echo — echo appends \n which Vercel stores literally, silently breaking string comparisons. See memory feedback_vercel_env_newline.md.
Phase 7: Voice agent vertical
Clone voice-agent-livekit/verticals/church/ into voice-agent-livekit/verticals/<vertical>/.
Files to create:
__init__.pyprompts.py—build_coordinator_prompt(),build_care_prompt()with vertical-appropriate tone + HEAR adaptationtools.py— vertical-specific tools (e.g., funeral:capture_arrangement_inquiry,request_director_callback,provide_chapel_info,flag_urgent_concern)agents.py—CoordinatorAgent+CareAgentclasses cloning the church structureREADME.md— design notes
Critical gotchas
@function_toolMUST haveNO_FALLBACK_NEEDED:comment or a real fallback chain — enforced byscripts/check-tool-add.shon push. Every tool that's a DB write or session op needs the comment. Place the comment INSIDE the function body within 40 lines of the@function_tool()decorator (the check script scans a 40-line diff window).sign_off_styleparam onbuild_shared_fragments()— pass"secular"for faith-neutral verticals (funeral, vet, law, shop). Default"faith_based"keeps church farewells ("God bless you!"). Don't change the default.- Tool inserts use shared-core table names +
verticalexplicit —voice_visitor_inquiries+tenant_id+vertical='<vertical>'. NOT the oldvoice_visitor_contactsview withchurch_id. Writes through the backward-compat view fail CASCADED CHECK OPTION for non-church verticals. - Knowledge base filter —
curation_status='approved'NOTis_active=true(the latter column doesn't exist on per-vertical knowledge base tables). - Register in
main.py:get_agent()— route phone numbers to the vertical'sCoordinatorAgent.
Deploy: C:\dev\lk.exe agent deploy --project cwa-voice --silent
Mandatory post-deploy verification (per churchwiseai-web/CLAUDE.md):
# Wait 90 seconds, then:
C:\dev\lk.exe agent logs --project cwa-voice --id CA_pX3Me4NK6qK8 --log-type deploy
# Look for "registered worker" + zero CRITICAL/ERROR lines
lk agent list status "Available" is MISLEADING — only logs tell the truth.
Phase 8: Admin dashboard scaffold
Create src/app/admin/<vertical>/[token]/:
page.tsx— server component, readsx-founder-tokenor token in params, queriespremium_<vertical>sWHEREadmin_token=tokenjoined to<vertical>s, renders dashboard shelllayout.tsx— setsdata-brand="<vertical>"for palette
Create server-only query helpers in src/lib/<vertical>-queries.ts (import 'server-only'):
getPremium<Vertical>ByToken(token)get<Vertical>ById(id)resolve<Vertical>Token(token)— returns{ premium, <verticalHome> }or null
Create client-safe types in src/lib/<vertical>-shared.ts:
<VerticalHome>type matching the tenant tablePremium<VerticalHome>type (omitadmin_token+ Stripe secrets — server-only fields)- Plan constants
- Label helpers
Client/server boundary discipline: if a 'use client' component needs a type or constant, import from -shared.ts, NEVER from -queries.ts. Per CLAUDE.md rule, server-only modules pull in supabase etc. and crash the browser bundle.
Minimum viable admin scaffold is an Overview tab showing subscription state, plan, admin token, contact info — full Calls / Settings / etc. tabs come later per customer demand.
Phase 9: Provision.ts vertical-aware
src/lib/outreach/provision.ts already has a VERTICAL_CONFIG dispatch map. Add an entry for your vertical:
const VERTICAL_CONFIG: Record<string, VerticalConfig> = {
church: { tenantTable: 'churches', premiumTable: 'premium_churches', tenantFkColumn: 'church_id', planPrefix: 'cwa_' },
funeral: { tenantTable: 'funeral_homes', premiumTable: 'premium_funeral_homes', tenantFkColumn: 'funeral_home_id', planPrefix: 'fwa_' },
// Add:
veterinary: { tenantTable: 'veterinary_clinics', premiumTable: 'premium_veterinary_clinics', tenantFkColumn: 'clinic_id', planPrefix: 'vwa_' },
};
That's it. The dispatch handles identity, subscription, organization_settings, voice agent, and AI-generated Q&A (routed to <vertical>_knowledge_base via a vertical branch in step 6 of the provision flow — see src/lib/outreach/provision.ts for the full pattern).
Gotcha — canned_responses is church-FK-locked
canned_responses.church_id has a hard FK to churches.id. Funeral path can't insert here. AI-generated Q&A for non-church verticals goes into <vertical>_knowledge_base instead. Plan accordingly when adapting provision.
Phase 10: Public render at /s/[slug]
The public render at src/app/s/[slug]/page.tsx now supports multi-vertical lookup (added 2026-04-22, PR #135). It tries getProWebsiteData(slug) first (church), falls back to getFuneralDemoData(slug) (funeral).
To add a new vertical:
- Create
get<Vertical>DemoData(slug)helper in the same file or a shared module - Add it to the fallback chain in both
generateMetadataandProWebsitePage - The helper returns a
PremiumChurch + Church-shaped adapter so the existingServiceBusinessTemplaterenders without modification. Setwebsite_template='service_business'on the adapter to force the prospect-demo branch (notUnifiedTemplatewhich assumes church fields).
Known render gap — FA-057
The current ServiceBusinessTemplate is MINIMAL — good for "doesn't 404" but not for serious prospect conversion. Full-parity public demos (branded hero, sections, interactive chatbot, live voice demo) are tracked under FA-057 in C:/dev/FOUNDER_ACTIONS.md. Funeral chatbot widget is explicitly disabled in the adapter (chatbot_enabled=false) because <vertical>_knowledge_base → unified_rag_content sync isn't wired yet. New verticals will inherit the same gap until FA-057 ships the pattern.
Phase 11: Knowledge + documentation
Step 11.1 — Product overview
Create knowledge/products/<vertical>wiseai/overview.md following the FuneralWiseAI template:
- Status (domain, codebase, status, deploy branch)
- Positioning (tone, palette, value proposition)
- Pricing (with canonical plan keys)
- Pages (site map)
- HEAR protocol application
- Technical architecture
- Strategic role
Step 11.2 — Decision doc (if significant departure from existing patterns)
If the vertical needs custom treatment (e.g., multi-clinic-chain support for vet, per-attorney branding for law), write a decision doc at knowledge/decisions/<date>-<vertical>-<shape>.md.
Step 11.3 — Product knowledge
INSERT INTO product_knowledge (category, question, answer, keywords, priority, vertical)
VALUES ('<vertical>wiseai', 'What does <Vertical>WiseAI do?', 'Answer...', ARRAY['keyword1'], 8, '<vertical>');
Run validate_product_knowledge() after — per feedback_verify_pk_after_update.md.
Step 11.4 — Update indexes
knowledge/INDEX.md— add pointers to the new product + decision docsknowledge/products/README.md— add the vertical to the Product Map + family diagramC:/dev/CLAUDE.mdquick-reference line — include the vertical's pricingknowledge/data/products.yaml— add entry, runpnpm derive
Phase 12: End-to-end validation
Once all the above is in place, prove the pipeline works end-to-end:
FOUNDER_TOKEN=$(vercel env pull .env.vercel.tmp --environment=production && \
grep "^FOUNDER_TOKEN=" .env.vercel.tmp | sed 's/^FOUNDER_TOKEN=//' | tr -d '"')
curl -sS -X POST "https://churchwiseai.com/api/outreach/scrape" \
-H "Content-Type: application/json" \
-H "x-founder-token: $FOUNDER_TOKEN" \
-d '{"url":"https://example-vet-clinic.com/","vertical":"veterinary"}'
Verify via Supabase:
- New row in
veterinary_clinics(notchurches) - New row in
premium_veterinary_clinicswithplan='vwa_demo_prospect' tenant_voice_agentsrow withvertical='veterinary'organization_settingsrow withvertical='veterinary'veterinary_knowledge_basehas AI-generated Q&A rows- Public URL at
/s/<slug>renders (minimal template — FA-057 upgrades this)
Phase 13: Registry + launch
- Update
C:/dev/FEATURE_REGISTRY.md— mark vertical launch featurescomplete - Deploy voice agent (requires founder approval)
- Test end-to-end from a fresh prospect: visit domain → marketing pages → chatbot → voice demo
- Document in
C:/dev/DECISION_LOG.md - Record in
C:/dev/ACTIVE_WORK.md— new workstream for outreach campaigns or close the launch workstream
Verification checklist
- Domain resolves to the vertical's home page with correct branding
- Checkout flow uses the correct Stripe price IDs
- Voice agent routes phone calls to the correct
<vertical>handlers (verified vialk agent logs) - Scrape-and-demo provisions into vertical-specific tables (not church tables)
-
/s/<slug>renders for vertical demos (not 404) -
ops_error_reportsis clean 30 minutes post-launch -
voice-healthcron returns HEALTHY within 5 minutes of voice deploy -
product_knowledgefor the vertical passesvalidate_product_knowledge()
Known follow-up gaps (shared across all verticals)
These are tracked as separate FAs and shouldn't block the launch, but will matter before real-prospect outreach:
| FA | Gap |
|---|---|
| FA-057 | Full-parity public demo (branded hero, sections, interactive chatbot, live voice demo). Current ServiceBusinessTemplate is minimal. |
| (pending) | <vertical>_knowledge_base → unified_rag_content sync so chatbot can read per-tenant FAQs |
| (pending) | Per-vertical shared demo phone number + dispatch rule for live voice demos |
See Also
- Decision: Multi-vertical tenant architecture
- Architecture: Multi-vertical schema
- Pricing Change Runbook
- FuneralWiseAI Overview — the reference vertical
- WiseAI Agency Overview — the meta-brand
- Knowledge Maintenance Runbook
- Memory:
feedback_never_migrate_before_audit.md(don't repeat M2's column-rename incident) - Memory:
feedback_vercel_env_newline.md(useprintfnotechofor env vars)