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 screenCrisis words checked by code before anything else. Crisis reply must contain 000 + Lifeline — enforced, not hoped for.
2 · Facts injectedRelevant approved answers from kb.json are pre-attached. The model may only state facts that come from there.
3 · Claude repliesTools: search_kb, save_lead, show_contact_form, flag_human. One question per message, two-strike rule.
4 · Cages appliedCode 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
| Layer | What it runs on | Why it holds up |
| Services | 3 independent Node.js + Express processes: web channel :3011, Messenger channel :3012, CRM :3020 | A channel crashing never takes the CRM down; each restarts alone |
| Storage | SQLite (WAL mode) per service; CRM DB is the source of truth | Right size for thousands of conversations/year; continuous backup via Litestream at deploy |
| Writes | One ingest API · API key per channel · idempotency keys · raw event log | New channel = new adapter + new key. CRM core never changes |
| Trust | Field-level provenance ledger + evidence-gated AI + human-only states + undo | Every value answers "who wrote this and based on what" |
| Security | Meta HMAC-SHA256 · Twilio HMAC-SHA1 · session login · scoped keys · access log · retention sweep | Privacy Act (health information) posture built in |
| AI | Claude (claude-sonnet-5) for: bot replies, transcript analysis, drafting, Q&A — each behind its own guardrails | Model 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.