Longitudinal Health Record (LHR) — product overview
Audience: product, clinical, support, and engineering leads who need to understand what LHR is, what it does for users, and how the pieces fit together.
Status: Same-tenant LHR views shipped in CMS. Cross-tenant timeline + JIT consent live. Adherence capture and shared-package extraction tracked as follow-ups.
Related: CMS developer guide · Patient-app developer guide · QA guide · Cross-tenant patient experience
What is LHR?
The Longitudinal Health Record is the patient's full health story over time, surfaced in two places:
- Patient app (
patient/— the standalone Vite + React app): the patient's own view, with self-reported entries (vitals, symptoms, medications) alongside clinician-authored records from every clinic they've been to. - CMS (the clinic-facing app at
apps/cms/): the clinician's view of the same patient, on the patient detail page and inside an active encounter via a Sheet.
Same data model, same view components, two surfaces. A symptom logged in the patient app shows up to the doctor inside CMS with a "patient-reported" source chip. A prescription written in CMS shows up in the patient app with the prescribing clinic's name.
What the user sees
Patient app
A single Medical Records page with eight tabs: Timeline, Symptoms, Vitals, Problems, Medications, Correlations, Visits, Lab Results. Each row carries a source chip (self-reported / clinical-imported / device / external) and, for clinic-authored rows, the clinic name. There's also a "Manage clinic access" page under Settings that lists every clinic the patient has appeared at and lets them grant or revoke that clinic's access to records from other clinics.
CMS
A "LHR" tab on the patient detail page (the 6th tab) and a "View patient history" Sheet inside the encounter page. Both render the same eight tabs as the patient app, against the same data, with two clinic-only affordances:
- 3-dot menus on Problems and Medications rows let the clinician link conditions to medications and vice versa.
- Import-to-encounter on rows from other clinics (gated by patient consent) copies the row into the current tenant's clinical record as
clinical_importedprovenance.
Cross-tenant consent
A doctor at Clinic B can see Clinic A's records about a shared patient only if the patient has granted consent. The flow:
- Doctor at Clinic B opens the patient's LHR. Same-tenant data shows immediately. A banner offers to "Request access to records from other clinics".
- Doctor clicks request → patient gets a notification (SMS + push) and a banner in the patient app.
- Patient grants → consent artifact is created (30-day per-tenant time-boxed), and other-clinic rows appear in Clinic B's LHR.
- Patient can revoke any time from the "Manage clinic access" page in the patient app. Already-imported rows persist; future reads are blocked.
The patient can also pre-grant access from "Manage clinic access" before a doctor asks — useful when the patient is referring themselves or wants to share proactively.
How the surfaces fit together
┌─────────────────────────────────────────────────────┐
│ Patient (the actual person) │
└────────────────────┬────────────────────────────────┘
│
┌──────────────┴──────────────┐
│ │
Patient app CMS app
(own records) (clinic's view)
│ │
▼ ▼
┌──────────────────┐ ┌───────────────────────┐
│ patient.* │ │ clinical.* │
│ (user-owned, │ │ (tenant-scoped, RLS, │
│ no RLS) │ │ FHIR-aligned) │
└──────────────────┘ └───────────────────────┘
│ │
└─────────────┬───────────────┘
│
▼
Cross-tenant aggregator + consent gate
(Clinical::Patients::LongitudinalRecordService)Same React view components render on top of either schema thanks to a backend transformer that emits the patient-app row shape from clinical.* reads.
What's in each tab
| Tab | What it shows | Where it comes from |
|---|---|---|
| Timeline | Mixed chronological summary across all resource types and all clinics the patient has visited (where consent exists). | Clinical::Patients::LongitudinalRecordService (cross-tenant) |
| Symptoms | Symptom log — patient-reported in the patient app, clinician-captured observations in CMS. | patient.symptoms (patient app) or clinical.observations (CMS) |
| Vitals | Trend charts (line + bar) for BP, heart rate, weight, temp, glucose, SpO2, respiratory rate, pain score, BMI. Out-of-reference-range points flagged. | patient.vitals or clinical.observations (category=vital_signs) |
| Problems | Active conditions with severity badges, ICD code, and linked medications. Resolved conditions collapsed below. | patient.conditions or clinical.conditions |
| Medications | Active + past medications with dosage, prescriber, and (eventually) adherence. | patient.medications or clinical.medication_requests |
| Correlations | Plots a symptom against a medication to spot patterns. | Derived from Symptoms + Medications |
| Visits | Chronological encounter history with practitioner, facility, reason. | clinical.encounters |
| Lab Results | Lab reports — uploaded by patient or attached by clinic. | patient.documents or clinical.documents (lab-typed) |
Design principles
Patient owns the data
The patient is the canonical owner of their health story. Clinical-authored rows are also visible to the authoring facility (a doctor needs access to what they wrote), but the patient sees everything. Patient-reported rows are patient-only unless explicitly shared. Cross-tenant reads are time-boxed and revocable.
One surface for both audiences
A symptom shown to the patient and to their doctor renders in the same component. The backend translates clinical.* rows into the patient-app shape so CMS doesn't need duplicate views. This means features added on one surface appear on the other for free.
Source chips are first-class
Every row, every surface, carries a source chip: self-reported, clinical-imported, device, external. Clinicians know what they can trust as primary. Patients can tell what they entered versus what a clinic added.
Consent is just-in-time, not bulk
There's no "share my whole history with everyone" toggle. Consent is per-tenant, time-boxed, and either patient-initiated (pre-grant) or doctor-requested (JIT). This mirrors how ABDM works in India and keeps the privacy story explainable.
What's deliberately out of scope (for now)
- ABDM linkage: deferred per ADR-002. The data model is compatible when we wire it.
- Bulk consent: no "share everything with everyone" mode.
- Adherence capture UI: medications show a "coming soon" affordance where adherence would render. Backend returns
adherence_log: nil. Capture flow lives in the patient-app vertical. - Write-through from CMS to patient-reported tables: CMS only reads from
patient.*(via import-to-encounter, which copies intoclinical.*). Patient-reported tables remain patient-only writes. - Apple Health / Google Fit ingestion: device source exists in the schema and chips but no ingestion pipeline yet.
Where it lives in code
- Backend (same-tenant LHR view endpoints):
backend/domains/clinical/controllers/patients/lhr_views_controller.rb+ transformers underbackend/domains/clinical/services/patients/lhr_views/. - Backend (cross-tenant aggregator):
backend/domains/clinical/services/patients/longitudinal_record_service.rb. - Backend (consent):
backend/domains/identity/models/cross_tenant_consent.rb+Identity::CrossTenantConsentsController. - CMS frontend:
frontend/apps/cms/src/features/patientLhr/. - Patient app frontend:
patient/src/features/health-records/. - Shared types/hooks for cross-tenant reads:
frontend/packages/shared-services/src/domains/clinical/(theusePatientLongitudinalTimelineanduseCrossTenantConsenthooks).
For implementation details, see the CMS developer guide. For the patient app side, see the patient-app LHR guide. For test plans, see the QA guide.