Demo — not a medical device — synthetic data only

FHIR patient chart · Portfolio project

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.

The Anamnesis summary for LEFEBVRE, Claire, 68, on ward B3: the navy identity banner with her ID and her two allergies — penicillins, anaphylaxis, high criticality, and iodinated contrast, low — then her active and resolved problems, her current medications from the fictitious catalog, her vitals and recent notes, and the line saying this access is logged
01 · The need

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.

01

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.

02

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.

03

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.

04

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.

02 · The rules

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.

01

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.

02

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.

03

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.

04

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.

03 · The chart

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.

Prescribing for LEFEBVRE, Claire, named in a strip above the form with her weight and an eGFR of 41: Spirodane 25 mg chosen from the fictitious catalog, and the live safety checks beside it — a MAJOR interaction with Lorvatan, risk of hyperkalaemia, her latest potassium of 5.8 mmol/L flagged H; a MODERATE renal dose limit; the allergy check passed against penicillins and iodinated contrast
Prescribing: a MAJOR interaction and a renal limit, live
The dialog “This note changed while you were writing”: another session saved version 5 while this amendment was written against version 4; the server's text beside the draft, the request POST $amend with If-Match W/"4" answered 412 Precondition Failed, nothing saved or overwritten, and three ways out — discard the draft, review side by side, rebase on version 5
A stale amendment: 412, shown, never merged
The timeline of 5 November: an amendment to the signed ward-round note, a discharge plan still in draft, the signed note, a set of vitals with a blood pressure of 148/92 flagged H, and a renal panel with potassium 5.8 and creatinine 128 flagged H and eGFR 41 flagged L; above, filters by event type with their counts and a date range
The timeline, day by day, filtered by type
Lab results: each test's latest value with its unit and flag, and the potassium trend over a year on a time axis to scale, the reference range of 3.5 to 5.0 mmol/L drawn as a band and the abnormal points as triangles, the table of values below
Potassium over a year: the range a band, abnormal points triangles
The ward list: 18 inpatients, 2 admitted in the last 24 hours, 2 with a critical result, 4 whose allergies are not recorded; filters by ward, then each patient's identity, bed, allergies — “Not recorded” marked as such — results out of range, reason for admission and admitting doctor
The ward list, allergies on every row
The interoperability and regulatory awareness page: a notice that this is awareness, not a compliance claim, then the implemented HL7 FHIR R4 subset — read, search, create, update and _history for each resource, with its search parameters
Interoperability: the FHIR subset, no compliance claimed
04 · Choices

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
05 · How it's built

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.

01

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.

02

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.

03

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.

04

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.

06 · Status

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.