Skip to content

About

Reference patient portal on FHIR R4: Next.js, Clerk, Google Cloud Healthcare API

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Riverside Health patient app

A reference implementation of a patient portal built on FHIR. Patients sign in and see their own health record, book and cancel appointments, request medication refills, read test results and message their care team. Every read and write goes to a FHIR R4 store.

Riverside Health is fictional and so is all of the demo data. Do not put real patient data in a store you use with this app.

Stack: Next.js 16 (App Router), React 19, Tailwind CSS 4, Clerk for sign-in, and the Google Cloud Healthcare API as the FHIR store.

How it works

Browser ──▶ Next.js server ──▶ Google Cloud Healthcare API (FHIR R4 store)
             │  Clerk: who is signed in?
             │  publicMetadata.fhirPatientId: which Patient are they?
             └─ service account: the only identity that can touch the store
  • The browser never talks to the FHIR store. It reads the record from app/api/patient-data/route.ts and changes it only through the server actions in lib/patient-actions.ts. Each of them checks who is signed in before it does anything.
  • A Clerk user is linked to one FHIR Patient through publicMetadata.fhirPatientId, which only the server side can set, so a user can't point themselves at someone else's record (lib/patient-session.ts). Every write that takes an id from the browser checks that resource belongs to the signed-in patient first.
  • The server reaches the store as a Google service account. Locally that's a credentials file impersonating the service account; on Cloud Run it's the service account attached to the service (lib/fhir/server.ts).
  • FHIR is mapped to and from the shapes the UI renders in lib/fhir/to-fhir.ts and lib/fhir/from-fhir.ts. The record uses Patient, Practitioner, PractitionerRole, Organization, Location, Schedule, Appointment, Condition, AllergyIntolerance, Immunization, ImmunizationRecommendation, Medication, MedicationRequest, MedicationDispense, Task (refill requests), ServiceRequest, DiagnosticReport, Observation, Communication and Coverage.
  • Terminology. Medications and conditions are seeded with bare RxNorm and ICD-10-CM codes, the way a minimally coded source system would send them. lib/terminology/ resolves them against the free NLM APIs (RxNav and Clinical Tables) and adds the official display names when the record is written. Anything it can't resolve with confidence is tagged for review rather than guessed.
  • Medication scanner. /medications/scan takes a photo of a pill bottle or box, sends it to app/api/medication-scan/route.ts, where Claude reads what is printed on it and the same RxNorm resolver as above maps it to a drug. Both the transcription and the RxNorm match are shown on screen. Nothing is stored, but the photo does go to Anthropic, so don't scan a prescription label that carries a real patient's name. Needs ANTHROPIC_API_KEY.
Where What
app/ Pages: home, appointments, health record, medications, messages, test results, profile
components/ Shared UI: app shell, dialogs, charts
lib/fhir/ FHIR REST client, FHIR ⇄ app mapping, reading and writing one patient's record
lib/terminology/ RxNorm and ICD-10-CM lookups and enrichment
lib/medication-scan.ts Medication scanner: reads a package photo with Claude, then maps it to RxNorm
lib/seed.ts The synthetic demo record
scripts/ Command-line tools (see Scripts)

Setup

You'll need Node.js 20.9 or later, a Google Cloud project with billing enabled, the gcloud CLI, and a free Clerk account.

npm install
cp .env.example .env.local

1. Clerk

Create an application in the Clerk dashboard and copy its publishable key and secret key into NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY in .env.local. The other Clerk variables are already filled in.

2. Google Cloud: FHIR store and service account

Replace PROJECT_ID and YOUR_EMAIL below with your own values. These commands create a dataset and an R4 FHIR store, both named demo-fhir-store in us-central1, plus a service account that can read and write that store and nothing else.

gcloud services enable healthcare.googleapis.com iamcredentials.googleapis.com --project PROJECT_ID

gcloud healthcare datasets create demo-fhir-store --location us-central1 --project PROJECT_ID
gcloud healthcare fhir-stores create demo-fhir-store --dataset demo-fhir-store \
  --location us-central1 --version R4 --project PROJECT_ID

gcloud iam service-accounts create patient-app-server \
  --display-name "Patient app server" --project PROJECT_ID

# The service account may read and write this one store.
gcloud healthcare fhir-stores add-iam-policy-binding demo-fhir-store --dataset demo-fhir-store \
  --location us-central1 --project PROJECT_ID \
  --member serviceAccount:patient-app-server@PROJECT_ID.iam.gserviceaccount.com \
  --role roles/healthcare.fhirResourceEditor

# You may act as the service account, so the app can run as it on your machine.
gcloud iam service-accounts add-iam-policy-binding \
  patient-app-server@PROJECT_ID.iam.gserviceaccount.com --project PROJECT_ID \
  --member user:YOUR_EMAIL --role roles/iam.serviceAccountTokenCreator

Set FHIR_STORE in .env.local to the store's full name:

FHIR_STORE=projects/PROJECT_ID/locations/us-central1/datasets/demo-fhir-store/fhirStores/demo-fhir-store

3. Google Cloud credentials

The app reads Application Default Credentials. Create a set that impersonates the service account, in its own folder so it doesn't replace any credentials you already use for other projects:

CLOUDSDK_CONFIG="$HOME/.config/gcloud-patient-app" gcloud auth application-default login \
  --impersonate-service-account=patient-app-server@PROJECT_ID.iam.gserviceaccount.com

Then set GOOGLE_APPLICATION_CREDENTIALS in .env.local to the file it wrote, as an absolute path, for example /Users/you/.config/gcloud-patient-app/application_default_credentials.json.

If your Google account belongs to an organization that requires periodic reauthentication, this sign-in expires: the app stops loading the record and the terminal running npm run dev logs "Google sign-in for the app has expired". Run the same command again, then restart npm run dev: the server keeps the old sign-in in memory until it restarts.

4. Seed a patient and link your user

npm run fhir:seed

This creates a demo patient in the store and prints its id. Start the app with npm run dev, open http://localhost:3000 and sign up. Then link your new Clerk user to the patient: in the Clerk dashboard, open Users, then your user, then Metadata, and set Public metadata to

{ "fhirPatientId": "<the patient id the seed printed>" }

Reload the app and you'll see the demo record. The seed script also prints the equivalent Clerk CLI command if you'd rather link from the terminal.

Scripts

Command What it does
npm run dev Start the app at http://localhost:3000
npm run fhir:seed Create a new demo patient and print its id
npm run fhir:seed -- --reset <id> Put an existing patient back to the demo record
npm run fhir:check Integration test against your store: seeds two throwaway patients, runs every write the app makes, checks one patient can't touch the other's record, then deletes both
npm run terminology -- lookup "Lipitor 20 mg tablet" Resolve a drug to an RxNorm clinical drug (also --rxcui, --ndc, --icd10)
npm run terminology -- enrich <id> Show what terminology enrichment would change in a stored record (add --write to save it)
npm run terminology:check Test the terminology service against the live NLM APIs; needs only network access
npm run lint ESLint

License

MIT

About

Reference patient portal on FHIR R4: Next.js, Clerk, Google Cloud Healthcare API

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages