Back to Portfolio

AlfaCura

AI-matched physiotherapy platform for the Italian market — five-layer clinical triage bot, specialization-aware matching, full physio practice suite.

ReactReact NativeFastAPISupabaseClaudeStripe
Visit live site

Client

AlfaCura

Industry

AI healthcare / Italian physiotherapy

Duration

Full build · 2025–2026

AlfaCura · Italian healthcare AI platform·2025–2026

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.

FieldDetail
RoleLead full-stack engineer — architecture, AI pipeline, web + mobile, data model, deployment
StackReact · 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

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_topic so 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_state column 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:

AreaWhat it does
AgendaGoogle Calendar-style day/week/month views. Respects configured working hours, draws blocked slots as gray overlays, colors appointments by status.
PazientiPatient CRM with filters (new / with appointments / archived), per-patient medical record store.
FinanzaInvoices with auto-generated PDFs, revenue analytics.
AbbonamentiStripe subscriptions with hosted checkout + customer portal.
ConsensiSigned consent documents, legally traceable via audit trail.
RecensioniOnly from patients who actually completed a visit.
StaffMulti-user clinics with role-based access.
Chatbot AIA dedicated AI assistant per physio that answers patient questions 24/7 on their behalf — configurable tone, specializations, answer bounds.
Zone di servizioGeographic 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.

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.