AlfaCura
AI-matched physiotherapy platform for the Italian market — five-layer clinical triage bot, specialization-aware matching, full physio practice suite.
Client
AlfaCura
Industry
AI healthcare / Italian physiotherapy
Duration
~110 commits across the feature build
AlfaCura is an Italian healthcare AI triage platform that has processed 13,000+ patient conversations in production — 10,000+ in Italian and 3,000+ in English. Patients describe symptoms in natural language; the system red-flags emergencies, extracts clinical entities, and matches each patient to the best-fit physiotherapists. The core is a five-layer safety pipeline: deterministic regex emergency detection runs before any LLM inference, so the model never gets the chance to mishandle a red-flag case. Built solo as lead full-stack engineer on FastAPI, PostgreSQL, and the Anthropic Claude API (Haiku, chosen over Gemini for stronger Italian clinical comprehension), with a React web app and a React Native mobile app on a shared Supabase backend. PHI is encrypted at rest with signed consent and a full audit trail.
Overview
The product
A full-stack healthcare marketplace for the Italian physiotherapy market. Patients describe symptoms in natural Italian to an AI triage bot; the system red-flags emergencies, extracts clinical entities, and matches each patient to the three physios best suited to them. Physios get a dashboard that runs their whole practice — agenda, patients, finance, consent forms, 24/7 AI assistant.
| Field | Detail |
|---|---|
| Role | Lead full-stack engineer — architecture, AI pipeline, web + mobile, data model, deployment |
| Stack | React · React Native (Expo Router) · FastAPI · Supabase (Postgres + Auth + Storage + RLS) · Anthropic Claude · Stripe · Netlify |
| Scope | ~40k LOC across web, mobile, Python bots, and 32 Postgres migrations · ~110 commits |
The problem
Italian patients looking for a physiotherapist today pick one from a directory, hope the specialization matches, and often discover mid-visit that the physio doesn't actually treat their condition. Physios on the other side waste calendar slots on non-targeted patients, field the same WhatsApp questions all day, and juggle bookings, invoices, consent forms and reviews across five disconnected tools.
The brief was to build a single product that:
- Collects a patient's symptoms in conversational Italian, safely.
- Matches them to the three best-fit physios (specialization, zone, availability — not just distance).
- Gives the physio a management suite that replaces the five tools.
- Stays compliant with Italian healthcare data rules — PHI encrypted at rest, signed consent, audit trail.
What I built
1. The triage bot — 5-layer Italian clinical pipeline
triage_bot.py (~2,800 LOC, Python/FastAPI) replaces a naive "send the message to the LLM" approach with a deterministic pipeline:
- Layer 1 — Safety detector. Hardcoded regex for cardiac, stroke, self-harm and other emergency phrasings in Italian. Catches the cases an LLM must never fumble.
- Layer 2 — Intent router. Rules-first, LLM fallback. Distinguishes
data_response,question,crisis_signal,off_topicso the dialog manager knows what to do next. - Layer 3 — Entity extraction. Italian-language NER for age, temperature, pain scale, duration, body part, gender, contextual preferences.
- Layer 4 — Dialog manager. State machine with 9 red-flag investigation protocols. Stateful across turns via a
dialog_statecolumn persisted in Postgres — the conversation survives refreshes. - Layer 5 — LLM response generator. Anthropic Claude with a tight system prompt, only invoked once the previous layers have structured the context.
The pipeline was originally written as Deno Edge Functions, then migrated to Python/FastAPI on subject-matter advice that Python would give better clinical reasoning. Frontend has a single env-var switch (VITE_PYTHON_BOT_URL) and backward-compatible endpoints at /functions/v1/claude-triage — so either backend can serve at any time and rollback is instant.
Why Claude (Haiku) over Gemini
The model was chosen, not defaulted. On Italian clinical phrasing — where a misread symptom is a safety issue, not a UX bug — Claude Haiku gave noticeably stronger comprehension of colloquial Italian and clinical context than Gemini in side-by-side testing, at a latency and cost that suited a per-conversation triage flow. Evaluating rather than reaching for the default is the difference between a demo and a production clinical system.
2. The matching algorithm
After triage extracts the clinical picture, an availability scanner (availability_scanner.py) walks every professional's configured working_hours, subtracts booked appointments and blocked_slots, and ranks the intersection of:
- Clinical specialization — filter by pathology tags, not just a free-text specialty field.
- Service area — only physios whose declared zones cover the patient's address.
- Real-time availability — slots calendar-synced in real time.
- Review quality — only feedback from verified visits (review rows tied to completed appointment IDs).
Output: the top 3 physios with a match percentage the patient can understand, plus an AI-generated explanation of why each is the right fit.
3. The professional suite (web + mobile)
26 screens on mobile alone, fully mirrored on the web dashboard:
| Area | What it does |
|---|---|
| Agenda | Google Calendar-style day/week/month views. Respects configured working hours, draws blocked slots as gray overlays, colors appointments by status. |
| Pazienti | Patient CRM with filters (new / with appointments / archived), per-patient medical record store. |
| Finanza | Invoices with auto-generated PDFs, revenue analytics. |
| Abbonamenti | Stripe subscriptions with hosted checkout + customer portal. |
| Consensi | Signed consent documents, legally traceable via audit trail. |
| Recensioni | Only from patients who actually completed a visit. |
| Staff | Multi-user clinics with role-based access. |
| Chatbot AI | A dedicated AI assistant per physio that answers patient questions 24/7 on their behalf — configurable tone, specializations, answer bounds. |
| Zone di servizio | Geographic coverage definitions feeding the match algorithm. |
4. The patient suite
A medical archive that does more than store PDFs:
- Uploads (referti, analisi, documents) auto-routed through AI and filed into ordered sections.
- History travels with the patient across physios.
- Appointments, consent forms, messages and the AI-triage chat all live in one timeline.
5. Security & compliance
Italian healthcare data is PHI. Migration 032 (phi_encryption.sql) + 033 (auto_encrypt_triggers.sql) + 034 (drop_plaintext_columns.sql) migrated the entire patient dataset to encrypted-at-rest PHI columns via Postgres triggers, then dropped the plaintext columns in the same deploy. Audit trail (audit_trail.sql) logs every read/write on a patient record. RLS policies enforce that a physio can only see their own patients.
6. Mobile app (React Native + Expo Router)
The mobile experience isn't a thin web wrapper — it's a first-class React Native app with:
- Auth-first flow with a guest mode: unauthenticated users can use triage and search; gated areas render an inline "Accedi / Registrati" card instead of a hostile redirect.
- Contextual drawer: the left drawer adapts to the current section — on Agenda it shows Oggi/Settimana/Mese toggles + hour config; on Pazienti it shows filters — while the bottom tab bar handles top-level navigation.
- Agenda config: auto-creates the professional record on first use (fixing a "profile not configured" dead end), persists weekly working hours to a JSONB column, manages blocked slots against the same table the matcher reads from.
Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ CLIENTS │
│ Web (React + Vite) Mobile (RN + Expo Router) │
│ · LandingPage · Professional suite (26 screens) │
│ · Professional dashboard · Patient suite │
│ · Patient dashboard · Guest mode │
│ · Triage chatbot · Triage chatbot │
└──────────────────────┬──────────────────────────┬───────────────────┘
│ │
▼ ▼
┌───────────────────────────────────┐ ┌─────────────────────────────┐
│ Python Bot Service (FastAPI) │ │ Supabase Edge Functions │
│ · triage_bot.py (5-layer) │ │ · Stripe checkout/webhook │
│ · professional_chat_bot.py │ │ · Invoice PDF generator │
│ · availability_scanner.py │ │ · Notifications/reminders │
│ · Anthropic Claude client │ │ · Consent signing │
└──────────────────┬────────────────┘ └──────────────┬──────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ Supabase (Postgres) │
│ 32 migrations · RLS on every table · PHI-encrypted columns │
│ · professionals · patients · appointments · messages │
│ · services · blocked_slots · invoices · consent_documents │
│ · reviews · notifications · audit_trail · patient_chats │
└─────────────────────────────────────────────────────────────────────┘Hard problems I solved
Prompt-injection defense inside a clinical chatbot
The bot reads patient free text, which is hostile input by definition. The pipeline treats the LLM as the least-trusted component: safety regex gates run before any LLM call, and the dialog manager — not the LLM — decides when to escalate, ask for more info, or hand off to a human.
Stateful conversations on a stateless backend
Added a dialog_state column to patient_chats; every turn round-trips the serialized state. Refreshing the page, switching devices, or coming back three days later resumes the exact conversation position.
"Profile not configured" dead end
The onboarding flow wrote to professionals.id but the agenda read from professionals.user_id, so the agenda screen was permanently empty for new physios. The fix: the agenda config screen now auto-inserts the row on first use, bridging the two assumptions and persisting weekly working hours + blocked slots that the matcher immediately picks up.
Two nav systems that used to duplicate each other
The mobile app originally had identical links in the left drawer and the bottom tab bar. Rewrote the drawer as a ContextualDrawerContent that scopes its entries to whatever section is currently active, so the tab bar does navigation and the drawer does in-section options.
Auth-first vs. public-first
The product needs both: a public chatbot that anonymous visitors can try, and a locked-down patient/physio app. Landed on auth-first with guest mode — guests pass through the route guard, gated screens render an inline sign-in prompt rather than a hostile redirect.
Results
13,000+
Conversations processed in production
10,000+
Italian conversations
3,000+
English conversations
5-layer
Safety pipeline before any LLM call
32
Postgres migrations
~40k
LOC across web, mobile & Python bots
- 13,000+ production conversations across two languages, every one gated by deterministic safety rules before the model was invoked.
- One product replaced five tools for the physio (directory, calendar, CRM, invoicing, consent software).
- Matching quality up, filler visits down. Physios see only patients targeted to their specializations.
- 24/7 responsiveness without hiring a receptionist — each physio's chatbot answers common patient questions on their behalf.
- Compliant by construction — PHI encrypted at rest, signed consent, audit trail, RLS.
Lessons
- Don't let the LLM near untrusted input unsupervised. Layering deterministic guards in front turns "AI chatbot" from a liability into a feature.
- Migrations are a product feature. The PHI-encryption rollout was three stitched migrations with triggers — deployed with zero plaintext ever reaching the final schema.
- Two clients, one contract. Keeping the web and mobile apps on the same Supabase tables (not separate microservices) made feature parity free and bug-fixes a single PR.
Built by Ojas Gangwal at Khaas. Want something similar?
Book a 30-min call →Need something like this?
Book a 30-min call. We'll scope your project together — no obligation.