This demo application showcases SMART on FHIR, a framework that allows healthcare apps to be launched from within an EHR and receive authenticated, scoped access to FHIR data.
SMART on FHIR is an open standard built on OAuth 2.0 that defines how clinical apps authenticate against a FHIR server and receive contextual information (e.g. which patient is in scope). It is designed to work across EHR vendors so a single app can be launched from any SMART-compliant system.
In this demo, there are three ways to launch a SMART on FHIR app:
- EHR launch from Medplum — Launch the app from Medplum's Apps tab; patient context is automatically provided.
- Standalone launch from Medplum — Open the app directly; after OAuth login the user picks a patient from a search list.
- SMART Health IT Sandbox — Test against a public sandbox with synthetic Synthea patients without a Medplum account.
- Node.js
^22.18.0or>=24.2.0 - npm
10.9.4or later (npm 11 is not supported by this project's lock file) - A Medplum account (only required for the Medplum launch flows)
# From the repo root
npm install
# Or from this directory
cd examples/medplum-smart-on-fhir-demo
npm installnpm run devThe app runs on http://localhost:8001 (avoids conflict with Medplum's dev server which uses 3000/3001).
npm run build- Sign in at [app.medplum.com]
- Create a new ClientApplication resource (https://app.medplum.com/ClientApplication)
- Fill in the required fields:
| Field | Value | Purpose |
|---|---|---|
| Name | SMART on FHIR Demo (or any label) |
Identifies the app in your project |
| Redirect URI | http://localhost:8001/launch |
OAuth callback for all SMART launch flows (EHR and standalone) |
| Redirect URI | http://localhost:8001/setup |
OAuth callback for the demo data setup page |
| Launch URI | http://localhost:8001/launch |
The URL Medplum opens when you click Launch from the Apps tab (EHR launch) |
Note: Medplum validates the redirect URI on every token request and will reject requests with unregistered URIs.
- Save and copy the Client ID (a UUID shown on the resource page)
Open src/config.ts and set:
export const MEDPLUM_CLIENT_ID = '<your-client-id>';The Medplum launch flows require patients in your project. The setup page creates 10 synthetic patients automatically.
- Navigate to [http://localhost:8001]
- Click Setup Demo Data
- Sign in with your Medplum account
- Wait for the confirmation message — 10 patients will be created
- Click Back to Home
The setup page signs you out automatically after completion so the SMART launch flow starts with a clean session.
- Go to [app.medplum.com]
- Go to the Apps tab in a Patient resource (https://app.medplum.com/Patient) and click on your ClientApplication
- Medplum redirects to
http://localhost:8001/launch?iss=https://api.medplum.com/fhir/R4/&launch=<token> - The app exchanges the launch token for an access token. Patient context is provided automatically
- You are taken directly to the Patient Dashboard for the patient in context
- Navigate to [http://localhost:8001]
- Click Launch with Medplum
- You are redirected to Medplum's OAuth login
- After login, you arrive at the Patient Picker (
/select-patient) - You are taken to the Patient Dashboard
- Use the ← Back to patients link at the top to return to the picker
- Navigate to [http://localhost:8001]
- Click Launch with SMART Health IT Sandbox
- You are taken to the sandbox's simulated EHR — select a patient from the picker
- After selection you are redirected back and taken to the Patient Dashboard
The demo app's patient dashboard displays:
- Patient header — Name, gender, date of birth, age, avatar
- Vitals summary — Latest blood pressure (systolic/diastolic in mmHg), weight (kg), and BMI (kg/m²) with measurement dates
- Blood pressure trends — Line chart of the last 10 readings plus a table with per-reading BP classification (Normal / Elevated / Stage 1 / Stage 2)
- Active conditions — Problem list items
The Setup Demo Data flow creates the following resources in your Medplum project, all tagged with https://medplum.com/smart-on-fhir-demo|demo for easy identification and cleanup.
Synthetic adults with name, birth date, and gender. Conformant to the US Core Patient profile.
Each patient receives:
| Type | LOINC Code | Profile | Count |
|---|---|---|---|
| Blood pressure panel (systolic + diastolic components) | 55284-4 / 8480-6 / 8462-4 |
US Core Blood Pressure | 5 readings, ~1 month apart |
| Body weight | 29463-7 |
US Core Body Weight | 1 (current) |
| BMI | 39156-5 |
US Core BMI | 1 (current) |
All observations use UCUM units (mm[Hg], kg, kg/m2) and are categorized as vital-signs.
Cardiovascular and chronic disease risk factors drawn from a pool of 12 conditions, coded with SNOMED CT:
| SNOMED Code | Display |
|---|---|
| 38341003 | Hypertension |
| 44054006 | Type 2 diabetes mellitus |
| 13644009 | Hypercholesterolemia |
| 414916001 | Obesity |
| 77386006 | Smoking |
| 59621000 | Essential hypertension |
| 40930008 | Hypothyroidism |
| 73211009 | Diabetes mellitus |
| 230690007 | Stroke |
| 22298006 | Myocardial infarction |
| 195967001 | Asthma |
| 13645005 | Chronic obstructive lung disease |
Conditions are created as problem-list-item with active / confirmed status, conformant to the US Core Condition Problems and Health Concerns profile.
This is a demonstration app and is not intended for production use.
- No client secret — The OAuth flow uses a public client (no
client_secret). This is correct for browser-based SMART apps but means the client ID alone is not a secret. - sessionStorage — Auth tokens (
smart_access_token) and context identifiers (smart_patient,smart_iss) are stored insessionStorage. This is cleared when the browser tab is closed but is accessible to JavaScript running on the same origin. - State parameter — A CSRF state parameter is generated with
crypto.randomUUID()and validated on callback. - No refresh tokens — The demo does not request or store refresh tokens. Sessions expire when the access token expires.
The iss query parameter was not present when the app loaded at /launch. This means the EHR did not initiate the launch correctly. Verify that:
- The Launch URI in Medplum is set to
http://localhost:8001/launch(not the root/) - You are clicking your ClientApplication from the Medplum Apps tab, not navigating directly to the launch URL
The app could not retrieve /.well-known/smart-configuration from the FHIR server. Common causes:
- The FHIR server URL (
iss) is incorrect or unreachable
The state value returned by the OAuth server does not match the value stored in sessionStorage before the redirect. Common causes:
- The browser session was cleared between the authorization request and the callback
- The callback URL was opened in a different tab or browser
MEDPLUM_CLIENT_ID in [src/config.ts] is still set to the placeholder 'your-client-id'. Update it with the Client ID from your Medplum ClientApplication.
The OAuth server returned an error in the callback URL (e.g. ?error=access_denied). Check the full error description displayed in the UI. Common causes:
- The redirect URI registered in the
ClientApplicationdoes not exactly matchhttp://localhost:8001/launch - The requested scopes were denied
- The client ID does not exist in the project
Another process is already using port 8001. Either stop that process or override the port:
VITE_PORT=8002 npm run devNote: if you change the port you must also update the redirect URIs in your ClientApplication and in src/config.ts.
The app navigated to /patient but the expected sessionStorage keys (smart_access_token, smart_patient) are missing. This happens when:
- The page was refreshed after the session expired
- The URL was opened directly without going through the launch flow
Return to [http://localhost:8001] and launch again.
The following launch contexts may be added in a future update:
- Encounter launch — When the EHR provides an encounter context alongside the patient, the app would scope blood pressure readings and conditions to that specific encounter.
- Practitioner launch — When an EHR launches the app with a logged-in practitioner's identity (
fhirUser), the app would show the practitioner's patient list usinguser/*.readscope rather than a single patient in context.