A patient chart with the safety rules a real record lives by.
A clinician's chart on an HL7 FHIR R4 server written in NestJS and PostgreSQL: allergies always in view, prescriptions checked on the screen and again on the server, signed notes amended and never edited, conflicts shown instead of overwritten, and every read and write on the record.
Demo — not a medical device — synthetic data only.

A ward round, one patient at a time.
A physician opens a patient, scans allergies, problems, labs and vitals, prescribes, signs a note — then the next bed. What they act on has to be the right record, and the current one.
The right patient
Name, date of birth, sex and ID in a banner that never scrolls away, and homonyms flagged in search. Similar names and shared birth dates are a known wrong-patient pattern.
The allergies, always
On every patient route, in the banner, before anything else. “No known allergies” is a statement someone recorded, refused while an allergy is active; “not recorded” says so.
A record nobody overwrites
Two clinicians on one record must never silently erase each other's work, and a signed note must stay exactly what was signed.
A chart in seconds, for anyone
A reviewer arrives with no account: no login, synthetic patients only, nothing that leaves the server, and a banner on every screen that says it is a demo.
What the chart holds to.
A few rules every screen and every endpoint keeps, because in a clinical record the quiet failure is the dangerous one.
Identity before every write
The patient named on each form and irreversible button; switching with unsaved work names both patients; a second tab on another patient raises an alert; the API answers 409 to a write whose X-Patient-Context names someone else.
Friction in proportion to risk
MINOR collapses, MODERATE is acknowledged by signing, MAJOR needs a coded reason and a monitoring plan, CONTRAINDICATED blocks. The server runs the same checks again and refuses an unjustified order.
No silent overwrite
Every read carries an ETag, every update needs If-Match: without it 428, against a stale version 412, and the chart shows both versions side by side. Nothing is merged for you.
Every access on the record
An AuditEvent for each read, search, write and refusal, append-only by trigger, and a Provenance with each write. The access log shows them, patient by patient.
Built and running.
Stills of the live chart on its synthetic ward: a prescription and its checks, a conflict refused, the timeline, a trend, the ward list, the interoperability page.






Weighed, chosen, and why.
Each key decision, with the alternatives, the choice and its price. Each one is written down as a decision record in the repository.
- 01
Storing FHIR
- Weighed
- One append-only table · a GIN index on the whole body · an expression index per parameter · search-parameter tables
- Chosen
- A FHIR store written by hand on PostgreSQL: the current version and every version as JSONB, and search values extracted on write into typed token, string, date, reference and quantity tables, as HAPI FHIR does.
- Because
- A read is one row by key, a search is a join on indexed tables, and one registry declares each parameter and writes the CapabilityStatement, so the two cannot drift. Paid in write amplification: one write is 3 + N rows.
- 02
Validating every write
- Weighed
- The HL7 Java validator or HAPI · fhirpath with StructureDefinitions · profiles of its own
- Chosen
- Profiles as TypeScript objects — required elements, cardinalities, bindings, named invariants such as a UCUM unit on every quantity — published as StructureDefinitions, failures answered as a 422 OperationOutcome naming the exact element.
- Because
- No JVM beside the API on a small server, and error paths precise enough for the form to put each one on its field. Not a conformance-grade validator: FR Core would need the official one in CI.
- 03
Notes, signed and amended
- Weighed
- Composition · DocumentReference
- Chosen
- A note is a DocumentReference: a draft updated with If-Match, $sign by its author only, any PUT on a signed note answered 409, a correction through $amend, linked to the original and signed in turn.
- Because
- No new resource type, the one the French DMP indexes documents with, and immutability held by the server, not by the form. The signature is a server-side SHA-256, not a qualified electronic signature.
- 04
Who is acting
- Weighed
- A sign-in screen · a sandbox per visitor · a fixed practitioner on one shared record
- Chosen
- No login: Dr Alice Moreau acts by default, an Act as menu turns a tab into another synthetic practitioner, and the shared record is reset to the seed every night.
- Because
- Nothing between a reviewer and the chart, and two tabs as two clinicians on one record are how the 412 and “only the author signs” are shown. In production: Pro Santé Connect or a CPS card, and no Act as.
- 05
Where the data comes from
- Weighed
- Synthea · a seeded generator of its own
- Chosen
- A seeded TypeScript generator: 54 synthetic patients, an edge case written for each safety rule, LOINC for labs and vitals, its own code system for the rest, a fictitious drug catalog and interaction table.
- Because
- Synthea needs a JVM and emits SNOMED CT and RxNorm, whose licences do not fit a public demo — a check fails the build on either. The same seed gives the same bundles, byte for byte.
- 06
Documents and charts
- Weighed
- pdf.js and a PDF library · a chart library · the browser's viewer and Exalus
- Chosen
- Synthetic PDFs from a PDF writer of about 150 lines, shown in the browser's own viewer beside their text; trend charts on Exalus's scales and paths, copied in and owned.
- Because
- No dependency for either; the accessible version of a document is real text; and the charts follow the clinical rules exactly — dates to scale, the range as a band, gaps never bridged.
From the seeded generator to the signed note.
A generator that stands in for the hospital's history, local services that stand in for the terminology server, the drug database and the directory, a FHIR server and a chart: the boundaries a real system would draw.
Model
Dependency-free TypeScript shared by both sides: FHIR R4 types written from the spec, the display rules for doses, dates, names and results, and the prescribing checks the form and the API both run.
FHIR server
NestJS 12 on PostgreSQL through pg and plain SQL: read, vread, search, create, update, _history; the write, its Provenance and its AuditEvent in one transaction; terminology, drugs and directory simulated behind real interfaces.
Chart
Next.js 16 and React 19 with the React Compiler, Tailwind 4 on design tokens, one zustand store per open chart; keyboard-first, English and French, built for WCAG 2.1 AA.
Server
PM2 and nginx on my own server, a CSP that allows this origin alone, atomic releases with automatic rollback, the demo reset nightly; registered in Zeus, watched by Iknos.
What is built, and what a hospital would add.
Live as a demo, built ticket by ticket, each decision written down as it is taken.
Built · the chart
- Search and ward listBy name, date of birth or ID from the / key, homonyms flagged; the ward list with its counts and filters.
- Summary, timeline, resultsProblems, medications, vitals, notes, documents and their text; lab trends with ranges and flags; the access log.
- AccessibilityWCAG 2.1 AA, axe on every route, keyboard flows, never colour alone; mapped to RGAA, awareness and not a declaration.
Built · the record
- PrescribingAllergy, interaction, renal and age checks on the form and on the server, overrides with a coded reason and a plan.
- NotesDrafts, signing by the author, amendments with a reason, the conflict dialog on a 412.
- A versioned FHIR storeETag and If-Match on every write, every version kept, a Provenance per write and an append-only audit; tested with four writers racing until exactly one wins.
For a real hospital
- Identity and sign-inThe qualified INS through INSi, and Pro Santé Connect or a CPS card instead of Act as.
- Profiles and terminologiesFR Core declared and checked by the official validator, national terminologies behind the local interfaces.
- Exchange and hostingDocuments sent to the DMP, and HDS-certified hosting.
Open a patient,
and try to prescribe the wrong thing.
Find LEFEBVRE, Claire on the ward list, prescribe Spirodane beside her Lorvatan and see what a MAJOR interaction asks of you. Then amend a signed note from two tabs at once, and watch the second one refused.