Vitals
The previous page, wherever the patient is. One clinical record with two faces — a clinic tablet that works offline indefinitely and a patient phone holding a portable copy — built on facts that merge by union, never by overwrite.
- Dart
- Flutter
- C# / .NET 9
- Postgres
- ChaCha20-Poly1305
- Ed25519
- xUnit
The problem
A Nigerian primary health record is a paper card, and the card stays where it was written. A child immunised in one clinic and seen in another starts again; a mother’s antenatal history lives in a register a hundred kilometres from the ward she delivers in. Every digital attempt has assumed a network that a rural clinic does not have for three weeks at a time.
The hard problem is not the record. It is merging two records that were both edited while neither could see the other — two nurses on two tablets, offline for a week, both updating the same child. A conventional last-writer-wins system silently destroys clinical data there, and here silent data loss is not a bug, it is a patient harm. That one modelling decision is the technical core, and it was built first.
What I built
- Facts, not rows. Every observation is an immutable fact with a hybrid logical clock, an author and a device. Records merge by set union; a correction is a new fact that supersedes, never an edit; no table has a destructive operation for any role. Five merge invariants are property-tested across generated multi-device interleavings, and a harness test proves the last-writer-wins alternative loses facts. The .NET replica runs the same merge in C#, held to the Dart by a 200-world parity fixture — which caught a real defect the night it was written: a generator whose low bits never produced a supersession, so one invariant had been passing vacuously.
- The wedge. The national immunisation schedule as a versioned table with catch-up; a dose as a fact with the vial’s batch and expiry from its GS1 barcode; an expired vial refused before anything is written; the ward whiteboard with the furthest-behind first and a message draft for the mother; the A5 card as a PDF in the national layout; the reminder on the mother’s phone naming the next vaccine and its day.
- Vitals, antenatal, stock, the fridge. Readings as integers in fixed units with the pulse card on the patient header, the published range printed beside each number and the trend drawn behind it. Forms drafted on every keystroke and proved to come back after the tree is thrown away. The antenatal visit that cannot be recorded until all ten danger signs are answered. The stock ledger counted by tapping tiles, a unit issued with every dose in the same write, the fridge asked twice a day.
- Handing the record over. A share grant — which sections, to whom, until when — is a fact on the patient’s record and is enforced where the payload is built: a test gathers the frames off the screen the way a camera would, reads the record back, and finds the note that was never in the bytes. The animated QR in a frame whose edge fills in the brand gradient as the frames go by; every open of a record a fact the patient sees on their own phone; the emergency card on the lock face while they opt in and never on a clinic tablet.
- Glass with a floor. A glass-over-gradient-mesh design with three depths that each mean one thing, a solid twin for every surface and zero for every duration, read at act time — because the floor is a three-year-old 10" tablet with 3 GB of RAM in a room lit by one window. A contrast test composites every text colour on every fill over every wash. Five patient-face languages held complete by a gate; the clinic face stays English.
Decisions worth naming
- Nothing clinical is computed. Vitals presents records, trends and checklists. A pulse is marked outside range by comparing a number to a printed range, never named high; the danger signs are answered one by one and nothing sums them; a pack is on the list, not on the list or the list cannot say. A copy gate fails the build on the words that would cross the line — and was broken on purpose with “Give 5 mg” to see it bite — and a test greps the domain’s own source for score, risk, triage and severity. It caught the registry’s search score, which was not clinical and was renamed anyway.
- The backend is a replica with no authority. If a nurse needs it to see a patient, it runs on the tablet. The server merges what it is handed, in C#, and hands back what a tablet lacks; it decides nothing.
- The domain imports nothing. Not Flutter, not a clock, not randomness. A pure Dart package under a gate proved to fire, because every device and the server must compute the same bytes from the same facts.
- Honest about what is known. Last met another device: three days ago, never synced. Every language but English says a draft, not yet read by a speaker where it is chosen. The audit export is signed with the tablet’s own key and verified by a Python script with no dependencies, run in the test on a good file, a flipped byte and the wrong key.
- A phone found what fifty-eight widget tests had not. Twenty minutes on a simulator with a synthetic clinic: the whiteboard listed a mother as 9,855 days overdue for BCG, because the schedule had no idea it was a child’s. It is a domain rule now. The exit gates that say on a real device are there for a reason.
What is not finished
- Hardware. The BLE path; the QR handover — sender, camera receiver and a paste floor are built — watched between an Android and an iPhone on real handsets; the reference tablet, reminders as notifications with the phone offline.
- People. A clinician reading every screen line by line; a nurse who has never seen the app recording a visit unassisted at or below the paper baseline; a native speaker for each of the four languages, whose absence the language picker admits.
- The pilot. Two facilities, and the merge invariants run against their real records with no fact lost.