Skip to content

Patient Portal

Ankit Upadhyay edited this page Aug 13, 2026 · 1 revision

Patient portal

This page describes the patient-facing application: what exists on its branch today, and what it is being built toward. It is for anyone picking up portal work.

Status: in progress, not merged. apps/portal lives on the feat/patient-portal branch and is uncommitted working-tree state there. It is not on dev, it is not in the CI matrix, and nothing in this page should be treated as a stable contract. The rest of this page describes what is actually in that tree.

Why a separate app

The portal is the Patients pillar. It could have been routes inside the staff application, and it deliberately is not, for three reasons.

The audiences have nothing in common. A patient sees one person's record, occasionally, on a phone, without training. A clinician sees hundreds, all day, on a desktop, fluently. Sharing a shell would compromise both.

The blast radius differs. A bug in a staff screen shows a clinician the wrong column. A bug in a portal screen shows a patient someone else's record. Keeping the surfaces separate keeps their authorization paths separate too.

The vocabulary differs. A staff screen shows R50.9 and Hyperlipidemia, unspecified. A patient screen has to explain what that means without a clinician in the room.

What is in the tree today

Next.js 16, dev server on port 3300, using @openrunic/ui for its primitives with a small set of portal-specific components on top.

Six routes:

Route What it is for
/ Home: what is coming up and what needs attention.
/appointments Upcoming and past visits.
/health-record Results, medications, problems, and immunizations.
/messages Secure messaging with the practice.
/forms Intake and consent forms to complete.
/bills Balances and statements.

Portal-specific components:

Component Purpose
AppShell, PortalChrome, PageHeader The patient-facing shell, separate from the staff shell.
AsyncBoundary One place for loading, empty, and error states.
PlainTerm Renders a clinical term with a plain-language explanation attached.
RangeBadge A result value against its reference range, worded rather than only coloured.
ProgressMeter Completion state for a multi-step task such as an intake form.
Money Currency rendering from integer cents.
Notice An inline message that is not an error.

A lib/api/ layer mirrors the staff app's: typed client interface, an HTTP implementation, a mock implementation, and fixtures. Same reasoning, same shape, so a developer moving between the two apps does not have to relearn the data layer.

Design constraints specific to the portal

These are the constraints the portal work is being held to, and they explain several component choices above.

Plain language, sourced. A patient-facing explanation of a clinical term comes from a curated code-to-plain-language mapping keyed on the codes already stored. ADR-0004 settled this: no model generates patient-facing interpretation. The regulatory reasoning is that the non-device carve-out for decision support is written around supporting a clinician, so directing interpretation at a patient instead is the weakest possible position.

Never colour alone, and never a bare number. A result reads as a value, a unit, and a worded range state. RangeBadge exists so this cannot be forgotten.

Mobile is the primary case, not a breakpoint added afterwards. Most patients open a portal on a phone, once, after a visit.

A patient sees one record. Proxy access, where a guardian or caregiver acts for someone else, is modelled in the schema through RelatedPerson with an explicit portal-proxy flag rather than by sharing a login. That model exists; the portal does not yet implement it.

What is not built

Everything with a write path. Self-scheduling, secure message composition, form completion and electronic signature, and online payment are all product intent recorded in the schema and in the API's reserved aggregates, not shipped code.

Patient authentication does not exist. The API's principal model already includes patient as an actor type, which is the seam it will attach to, but there is no login, no enrolment, and no proxy handling.

The portal is not in CI. It has no coverage floor entry, no Sonar project, and it does not appear in the affected-workspace matrix.

Related pages

Clone this wiki locally