An MVP, live and in front of real users, in five working days. See how →

← All work

Product Engineering & AI

MedFlow

MedFlow is a multi-tenant healthcare operations platform where every clinic runs as an isolated workspace. It covers the full operational surface of a practice — scheduling, clinical charting, prescribing safety, invoicing and insurance, telemedicine, and an AI layer that includes a phone number patients can actually call. The hard part was never the feature list. It was building each of those so that the failure modes are safe ones.

{ Our Role }
Product design, full-stack engineering, AI systems, and production infrastructure
{ Timeline }
Ongoing — in active development through 2026
{ Scope }
Full-Stack Engineering · AI & Voice Systems · Billing & Payments · DevOps & Infrastructure
MedFlow
{ The Problem }

Clinical software carries a different class of risk to most SaaS. A tenancy bug does not leak marketing data, it leaks somebody's medical history. An AI feature that hallucinates confidently is not an amusing demo, it is a clinical hazard. An invoice numbering gap is not cosmetic, it is an audit finding. Two patients booked into the same slot is a person turned away at reception.

MedFlow needed to move fast across a very wide surface — charting, billing, insurance, video consults, voice AI — without any of those categories of failure being possible in the first place. The safety could not be a review process bolted on at the end; it had to be a property of the code.

{ Our Approach }

We designed each subsystem around its worst outcome and made that outcome structurally unavailable. Multi-tenancy is enforced by a shared workspace filter at the data layer rather than by remembering to add a clause. Clinical records are append-only with immutable versions, so an amendment adds history instead of destroying it. Money is stored as integer minor units and totals are always recomputed server-side.

The AI work followed the same rule. Rather than trusting a prompt to keep a model in line, capability is bounded by what the code will let the model reach — and those boundaries are asserted by tests, so a future contributor cannot widen them by accident.

We were also deliberate about what MedFlow would refuse to do. It codes diagnoses to ICD-10 but declines SNOMED and CPT rather than approximating them. Growth percentiles refuse to plot for ages with no reference data instead of extrapolating a line. Saying "no" precisely is more useful to a clinician than a confident guess.

{ The Solution }

The platform is a React 19 front end against an async FastAPI and MongoDB backend, with a background worker for PDF generation and scheduled jobs and a separate voice-agent process for telephony. Local development runs the whole thing under Docker Compose, including a MongoDB replica set so concurrency behaviour is exercised the way production sees it.

Clinics get scheduling with atomic slot booking, append-only charting with note templates, problem lists, medication reconciliation and allergy checking, ICD-10 coded diagnoses, vitals with growth percentiles, immunisations and referrals. Billing covers a service catalogue with price snapshots, a full invoice lifecycle, insurance splits with claim and settlement, refunds, and a Stripe-hosted patient payment portal. Telemedicine runs on LiveKit, and the same infrastructure powers an AI voice agent that answers a real phone number.

{ Inside the Build }
01

A phone number answered by software — with no access to charts

The AI voice agent (LiveKit SIP for telephony, Deepgram for speech-to-text, ElevenLabs for speech, Claude for reasoning) can look up, book, move, and cancel appointments. That is the entire tool registry — no tool it can call reads clinical data, and a test asserts it. The restriction lives in code, not in a prompt somebody could talk their way around. Callers are verified on name and date of birth before anything is disclosed, prompts are versioned data with the version recorded per call, call minutes are capped per workspace, and no audio is persisted.

02

AI governance before any data leaves the building

Every model call passes through a governance layer that checks per-workspace consent against versioned terms — a terms bump requires re-acceptance — and a monthly quota that returns a 429 with Retry-After when exhausted. The allow decision is deliberately separate from the model call, so the gate cannot be bypassed by a new caller wiring the client up directly.

03

Allergy checking where the boundary is the feature

The allergy checker matches recorded allergies on whole words only. It does not infer drug classes, and it warns rather than blocks. A verbatim disclaimer travels with every result and is asserted in the test suite, and the record stores whether the prescriber was warned — not just what was prescribed. A checker that quietly guessed at drug families would be more impressive in a demo and considerably more dangerous in a clinic.

04

Clinical records that cannot be quietly rewritten

Records are append-only with an immutable version history. An amendment requires a reason and an author and produces a new version rather than overwriting the old one, so the chart shows what was known at the time a decision was made — which is the question that actually gets asked afterwards.

05

Invoicing built for an audit, not a dashboard

Invoice numbers are gapless and sequential per workspace via an atomic increment. Issued invoices are immutable — corrections are credit notes. Amounts are integer minor units with totals recomputed server-side, and the lifecycle (draft, sent, part-paid, paid, void, written off) supports insurance splits where patient and insurer balances are tracked separately through claim and settlement. PDFs render on the worker so the request path stays fast.

06

Tenant isolation as a data-layer property

A shared workspace filter is applied at the query layer rather than per endpoint, email uniqueness is scoped per workspace, and regex inputs are escaped before they reach a query. The audit trail redacts values, never writes secrets, and enforces retention. Staff get TOTP two-factor with backup codes, and the operator console supports impersonation with a trail behind it.

07

Notifications that never leak clinical detail

One data-driven matrix decides every notification: durable in-app messages carry the detail, while email is restricted by allow-list to non-clinical content, and quiet hours are respected. Adding a notification type means adding a row, not another branch that might forget the email rule.

08

CI that runs the risky paths, not just the happy ones

Three parallel jobs: backend lint and pytest against a real MongoDB replica set — so concurrency tests such as double-booking actually run, with a guard that fails rather than silently skips them; frontend Jest plus an orphan-component check; and Playwright end-to-end. Production runs on Hetzner provisioned by Terraform with Caddy terminating TLS, images published to GHCR and pulled by the server.

{ Built With }

Frontend

  • React 19
  • TailwindCSS
  • shadcn/Radix
  • React Query
  • react-hook-form
  • zod
  • Recharts

Backend

  • FastAPI
  • MongoDB Atlas
  • motor (async)
  • Pydantic 2
  • JWT
  • bcrypt
  • Background worker

AI & Realtime

  • Anthropic Claude
  • LiveKit Cloud
  • LiveKit SIP
  • Deepgram STT
  • ElevenLabs TTS

Payments & Documents

  • Stripe Checkout
  • WeasyPrint
  • Resend

Infrastructure

  • Docker Compose
  • Terraform
  • Hetzner
  • Caddy
  • GHCR
  • GitHub Actions
  • Playwright
  • Sentry
{ The Results }
Atomic
Slot booking, never doubled
100%
AI calls consent & quota gated
Gapless
Per-workspace invoice numbering
{ Next Project }

T-HRMS

View Case Study