Moanv
Baton case study
Baton

Architecture

Baton

A solution-architecture summary — how the system is designed, and why. Verified from the source code.

Type of system
Multi-tenant clinical platform (patient portal, clinician console, public booking)
Users
Patients, clinicians and clinic administrators
Hosting
United Kingdom (self-hosted, Docker)
Main stack
FHIR backbone (Medplum), React, Next.js, PostgreSQL
Status
Live — in daily clinical use
Built for
Moanv's own product (first clinic live: Plexus Psychiatry)

The problem

A private mental-health clinic's work is a relay: booking, payment, ID check, questionnaires, the consultation, the report, prescribing, the GP letter, discharge. Stitched together from a booking tool, a spreadsheet and a separate records system, things get retyped and dropped between them — and it all has to hold up in a regulated setting handling sensitive health data and controlled-drug prescribing.

How it fits together

Users

Patients · clinicians · administrators

Front end

Patient app (Next.js) + staff console (React)

Tenant boundary

Backend / API

FHIR server (Medplum) + sandboxed server “bots”

Database

PostgreSQL 16 + Redis

Each clinic is a separate project with its own records and access policies, sharing one common library.

External services

  • Anthropic Claude — report drafting
  • Stripe — payments
  • Microsoft Teams (Graph) — video
  • Mailjet — email
  • NHS dm+d / ODS — medicines & GP lookup
Patients, clinicians and administrators use a Next.js patient app and a React staff console. Both talk to a Medplum FHIR server, where most logic runs as sandboxed bots, backed by PostgreSQL and Redis. Each clinic is an isolated tenant. External services handle AI report drafting, payments, video, email and NHS lookups; no patient data is sent to the NHS lookups.

Tech stack

Front end

  • Staff console: React 19, Vite, Mantine
  • Patient app: Next.js 15, React 19, Tailwind

Backend

  • Medplum (FHIR R4) on Node
  • Server logic as sandboxed Medplum bots
  • Patient-app server routes

Database

  • PostgreSQL 16
  • Redis 7 (both on Docker volumes)

Hosting

  • United Kingdom
  • Single Linux host, Docker Compose, nginx, Let's Encrypt TLS
  • Deployed via Azure DevOps over SSH

Third-party

  • Anthropic Claude (reports)
  • Stripe (payments)
  • Microsoft Teams via Graph (video)
  • Mailjet (email)
  • NHS Terminology Server & ODS (no patient data sent)

Key design decisions

Build on a FHIR backbone (Medplum), not bespoke CRUD

Why: Clinical data has a standard model; using it means records interoperate and access rules attach to the data itself, not just the screens.

Trade-off: A heavier backend and a real learning curve versus a simple custom database.

Multi-tenant from day one — a separate project per clinic

Why: Baton is a platform, not a one-off. A new clinic onboards onto the same system, and each clinic's records stay isolated.

Trade-off: More careful access-policy design up front than a single-clinic build would need.

AI drafts, humans decide

Why: Assessment reports are drafted only from a fixed evidence pack for one appointment, and a clinician signs. The AI never makes a clinical decision — deliberately staying outside medical-device territory.

Trade-off: The AI can't ‘finish’ a report; a clinician always reviews and signs.

No self-registration

Why: Nobody should create their own clinical account. Staff are invited; patients are created through booking; machines use OAuth client credentials.

Trade-off: An extra onboarding step, by design.

Own the whole pathway rather than buy point tools

Why: One record from booking to discharge means nothing is retyped between a booking tool and a records system.

Trade-off: We own the entire surface and its maintenance.

Security & data protection

  • Hosted in the United Kingdom.
  • TLS everywhere (Let's Encrypt, auto-renewing) with HSTS.
  • Access by role enforced on the server: a consultant sees only their own patients; an administrator sees no clinical notes.
  • Two-factor authentication required for staff.
  • Full audit trail — every read, search and write is recorded against the user; staff can create audit entries but never read or edit them.
  • No self-registration; staff are invited, machine access uses OAuth client credentials.
  • Payments are confirmed by a signed Stripe webhook on the server, never by the browser.
  • Server-side validation of every booking, including an NHS-number checksum and an 18+ rule.
  • Per-clinic AI keys; the model is given only one appointment's evidence pack and cannot reach anything else.

Built to hold up

Performance
A server-rendered patient app and a console that loads one patient chart at a time; media served directly by nginx.
Availability
A single UK host with a post-deploy health check; deploys are scripted and repeatable.
Scalability
Multi-tenant by project — a new clinic is added without standing up new infrastructure for it.

What we'd do next

  • Add a Content-Security-Policy across the clinical surfaces to complement the existing security headers.
  • Extend the automated access-control tests to cover every staff role, not only the patient boundary.
  • Formalise documented retention and deletion schedules for records, transcripts and audit logs.