How the lead system works, end to end

Three services, three ports, one source of truth. Measured write latency channel → CRM: ~2 seconds.

1A person gets in touch

Two doors in, both leading to the same place.
WEBSITE PATH · port 3011
Visitor opens the website widget
Chat bubble on the WordPress site. Visitor is anonymous, so the intake card asks three things up front: who the enquiry is for, NDIS plan status, and their mobile number.
intake card → one submit → bot
Conversation with the bot
Bot asks about their situation, shows a small form for name + optional email/time, answers FAQs from the knowledge base, closes warmly.
web prompt variant: forms exist, name must be asked
MESSENGER PATH · port 3012
Person taps a Facebook ad (or messages the Page)
Meta opens a Messenger thread and tells us: their first name, their permanent identity (PSID), and which ad brought them (referral.ad_id + our per-ad ref code).
webhook: signature verified · duplicates dropped by message id
Conversation with the bot
Greets by name ("Hello Sarah"). Who-for and plan status are tap-to-answer quick replies. Phone number is a one-tap share from their Facebook profile. No forms — they don't exist here.
messenger prompt variant: never ask name, never say "form"

2One brain answers both

Same pipeline code and knowledge base in each channel folder; only the prompt dialect and the rendering differ.
1 · Safety screen
Crisis words checked by code before anything else. Crisis reply must contain 000 + Lifeline — enforced, not hoped for.
2 · Facts injected
Relevant approved answers from kb.json are pre-attached. The model may only state facts that come from there.
3 · Claude replies
Tools: search_kb, save_lead, show_contact_form, flag_human. One question per message, two-strike rule.
4 · Cages applied
Code strips markdown/emoji, force-adds crisis numbers, extracts phone/email by regex so a lead can never be lost, blocks pressure after "stop".
fire-and-forget POST, never blocks the reply to the customer

3The CRM files it — automatically, in ~2 seconds

Port 3020 · the single source of truth. Nobody types anything.
One write door: POST /api/v1/ingest
API key per channel idempotency — a replayed event is accepted once raw payload logged (replayable forever) do-not-contact suppressed at the door

Identity resolution — is this a person we already know? Match by phone first, then email, then channel identity. The same person on Messenger yesterday and SMS today becomes one client record (proven in testing).

Provenance ledger — every field write is recorded: old value → new value, who wrote it (📥 channel🤖 AI👤 team member), the evidence quote, and the confidence. This powers the undo button and the hover-to-see-why tags.
SQLite WAL · contacts / leads / conversations / messages / field_values / events / tasks
90 seconds after the conversation goes quiet

4The AI analyst reads the whole transcript

Automatic analysis — with hard limits it cannot cross
Writes the summary, the next step for staff ("Call 0433… Thursday, she asked for the afternoon"), and an urgency level. May advance the stage only new → engaged → qualified and only forward.

must quote the customer's exact words as evidence, or it cannot move a stage cannot touch anything a human wrote low confidence = suggest only, write nothing no diagnoses or medications ever placed in fields circuit breaker: too many changes per hour → AI frozen

Meeting booked, agreement signed, active client, do-not-contact — these describe the real world, so only a human can set them.
claude-sonnet-5 · schema-forced output · quality gate: 30/30 eval assertions

5Your team works from the CRM

Reading is instant; every action is deliberate.
See
Dashboard: who to call today and why. Client folders: summary, full conversation, timeline of every change. Pipeline board. Search anything.
Act
Move stages when real things happen. Write team notes (AI can never edit them). Approve or discard AI-drafted SMS in the Outbox — nothing sends itself. Push a ready client to VisualCare with one button (double-push blocked).
Ask
AI Assistant chat: "Who should I call first?" · "Prep me for this call." Reads real data, answers with names and numbers, can never write. Same via MCP from Claude apps.

The loops that keep it honest

SMS replies flow back in
Lead texts back → Twilio webhook → same ingest door → appears in the same client folder. Cross-channel identity match is automatic.
POST /webhooks/sms/inbound · signature verified
Humans override bots
Staff reply from the Page Inbox → bot detects the echo and goes quiet on that thread for 48h. Undo restores any AI-written value. do-not-contact can never be flipped back by automation.
The world subscribes
Outbound webhooks (signed) announce lead.created / updated / status_changed — Zapier, n8n, Google Sheets plug in with zero new code. OpenAPI spec published at /openapi.yaml.

Technical foundation

LayerWhat it runs onWhy it holds up
Services3 independent Node.js + Express processes: web channel :3011, Messenger channel :3012, CRM :3020A channel crashing never takes the CRM down; each restarts alone
StorageSQLite (WAL mode) per service; CRM DB is the source of truthRight size for thousands of conversations/year; continuous backup via Litestream at deploy
WritesOne ingest API · API key per channel · idempotency keys · raw event logNew channel = new adapter + new key. CRM core never changes
TrustField-level provenance ledger + evidence-gated AI + human-only states + undoEvery value answers "who wrote this and based on what"
SecurityMeta HMAC-SHA256 · Twilio HMAC-SHA1 · session login · scoped keys · access log · retention sweepPrivacy Act (health information) posture built in
AIClaude (claude-sonnet-5) for: bot replies, transcript analysis, drafting, Q&A — each behind its own guardrailsModel swappable by env var; 66 + 242 + 30 automated assertions gate every change
Real-time write, measured today: customer message sent 13:32:54 → CRM record updated 13:32:56 (~2 s). The bot's reply never waits for the CRM (fire-and-forget), so a CRM hiccup can never slow a customer conversation. AI analysis lands ~90 s after the conversation goes quiet.