Lens Full-stack case study · clinical data & mobile UI
Engineering case study

Nine FHIR topics, one deterministic pipeline, and a screen a clinician can read in the time it takes to walk to the next bed.

Lens sits beside a hospital's FHIR store and compresses a sprawling patient record into a structured snapshot, then puts that snapshot in front of clinicians on a mobile app built for ward rounds. Everything below is running code — sign in, pick a list, switch patients, capture a note, and ask the assistant a question against the same snapshot data the real system would send to Claude.

9Topic snapshots
3Access roles
5Docker services
2-passImage retrieval
Open the simulator

A summarisation layer, not another EMR

Lens reads FHIR resources for a patient, sorts them into nine clinical topics — Admin, Alerts, Medications, Notes, Imaging, Labs, Obs, Orders, and Clinical Images — and produces a LensComposition: a compressed, structured snapshot written back into the same FHIR store.

A LensMap mirrors the organisation's physical location hierarchy; LensLists are the patient lists clinicians actually work from — a ward, a post-take list, a discharge-planning cohort. When a device loads a list, it pulls the latest composition for every patient on it, then stays current by running the identical toSnapshot() / merge() logic on-device as new resources arrive over the event bus — so the LLM is always querying state at least as fresh as what's on screen.

Python 3.12 FastAPI HAPI FHIR R4 PostgreSQL Kafka / Event Hubs Anthropic API APScheduler React Docker Compose

A deterministic pipeline, not a model in the loop

toSnapshot() and merge() never call an LLM. Every resource is standardised, routed, reduced to a delta, and folded into the patient's running snapshot with one of three merge strategies:

FLAT_REPLACE The new value replaces the old outright. Used for Admin demographics — a patient's current ward or bed doesn't need history, just the latest value.
UPSERT Matched by resource id and updated in place, or inserted if new. Used for Orders and Obs device metadata, where a request can be amended without becoming a new line item.
APPEND_WINDOW Appended to a rolling window, oldest entries dropped. Used for Obs vitals (48‑hour window, 6‑hour buckets) and Labs (1‑year window, bucketed by LOINC code).
flowchart LR
  A[("FHIR Store")] --> B["Reader
paginate & resolve refs"] B --> C["Idiosyncrasy Adaptor
per-org normalisation"] C --> D["Topic Split
route to 1+ topics"] D --> E["toSnapshot()
deterministic delta"] E --> F["merge()
FLAT_REPLACE / UPSERT / APPEND_WINDOW"] F --> G["LensComposition"] G --> A G -. "GET /patients/:id/composition" .-> H["RESTful API"] H -. rendered markdown .-> I["Mobile App"] I -- "/llm/query" --> J["Anthropic API"] J -- response + optional IMG: tags --> I I -- "/llm/image (2nd pass)" --> H

Nine topics, one snapshot per patient

Eight ride along as swipeable tabs in the app; the ninth — Alerts — surfaces as banner tags on every patient card, because an allergy or a resus status shouldn't need a tap to see.

A face for every patient, generated — not stored

A ward list full of similar ages, similar diagnoses, and near-identical surnames is exactly where a name-only avatar fails a clinician moving fast between beds. Every patient in Lens gets a small monochrome glyph instead — scannable by shape, not just by reading initials twice to be sure.

The glyph — a Patient Visual Identification icon, or PVIS — is derived deterministically from the patient's IHI number: a container silhouette (one of twelve), one to four satellite glyphs, and the patient's initials rendered on top. No image is stored anywhere and no lookup table is maintained.

n         = abs(IHI)
container = CONTAINERS[n % 12]          // 12 silhouettes
glyph     = GLYPHS[floor(n / 12) % 12]  // 12 satellites
count     = floor(n / 144) % 4 + 1      // 1–4 satellites

Two clinicians, two devices, weeks apart — same IHI in, same shield-and-three-eyes glyph out, every time. It's the same principle as the topic snapshots themselves: derive on demand, don't store and drift.

Explore the app

This is the real interaction model, running client-side against a mock ward of six patients (Margaret Chen fully populated, five more for list-switching). Switch roles to see access control in practice, or step through the flow on the right.

9:41

Who can see what

Every request passes through an access-filtering layer before data is returned. Role determines scope — not just what's on screen, but what the API will serve at all.

RoleLensListsLensCompositionNotes / ImagesLLMOrg settings
Healthcare Worker · HPI-POwn listsPatients on own listsWriteYesNo
Org Administrator · HPI-OAll org listsOrg-wideWriteYesYes
Patient · IHIOwn record only, limited topicsNoNoNo

Five ways to look at the same list

A Lens configuration decides which topics show, in what order, and in how much detail — a ward round and a blood-round pass over the same patient but need different information first. Try switching configs on the phone above; the tab order changes.

The built-in four cover the common cases; the fifth — Handover — is the point. Healthcare workers can build and save their own Lens configurations from within the app, tuned to a specific role, ward, or task, and their five most-used configurations surface automatically on the selection screen. A night registrar covering four wards, a diabetes outreach nurse, and a discharge coordinator all read the same underlying snapshots — through a different lens each.

Everything
Default view. All nine topic snapshots, full detail, default order.
Ward Round
Current medications, recent updates and results, obs and vital trends, active alerts and allergies — first.
Blood Test Review
Labs in full, unfulfilled orders, and just enough clinical context to interpret them — for working a whole list of results in one pass.
Longitudinal
All resources in chronological order across every snapshot — the story of the admission, not the topic split.
Handover
Custom configuration, user-created and saved. The five most-used are surfaced on the selection screen.

Five services, one Compose file

All local dev, all one command. The browser UI is the primary interface for building and demoing the mobile flow without a device.

serviceportrole
hapi‑fhir8080FHIR R4 store
postgres5432HAPI FHIR backend
lens‑api8000snapshot machine, reader/writer
restful‑api8001auth, access filtering, LLM proxy
lens‑ui3000React — browser sim of mobile app