Ship a SMART on FHIR App with Discovery and PKCE for Developers
· 12 min read

Ship a SMART on FHIR App with Discovery and PKCE for Developers

You can build a minimal browser SMART on FHIR app that launches from an EHR or standalone and reads patient vitals relatively quickly. The workflow has three moving parts: discovery, OAuth2 authorization with PKCE, and FHIR data access. Test it against a sandbox first, use the SMART JavaScript client to skip the boilerplate, and you’ll have a working app that pulls real patient resources long before you touch a production EHR.
TL;DR:
- The discovery phase requires fetching the well-known configuration document to avoid hardcoding endpoints, which can cause integration failures between sandbox and production.
- OAuth2 with PKCE must be correctly implemented by generating and verifying code challenges and verifiers; mistakes here are common causes of launch issues.
- Requesting
offline_accessscope is unnecessary for most user launches unless long-term background refresh tokens are needed, as sessions typically end with app closure.- Registering each environment with an exact HTTPS redirect URI and maintaining a JWKS URL for private key JWT authentication are critical for successful production deployment.
- Many errors, such as URI mismatches or incorrect scope requests, can be resolved by verifying registration details, using client libraries for URL building, and testing against sandbox environments first.
Table of Contents
- What You Need Before Starting This SMART on FHIR Tutorial
- How Does the SMART on FHIR Launch Flow Actually Work?
- Which SMART Launch Pattern Fits Your App?
- Building a Minimal Browser SMART on FHIR App: Step by Step
- Getting PKCE, Token Exchange, and Scopes Right
- Which Client Libraries and Sandboxes Should You Use?
- Your Production Checklist for SMART on FHIR Registration
- Common SMART on FHIR Errors and How to Fix Them
- What I’ve Learned Building SMART Integrations
- An Alternative Path: PHI-Safe FHIR Access With Medscrub
- Where to Go Deeper on SMART on FHIR
- Sources
What You Need Before Starting This SMART on FHIR Tutorial
Assume FHIR R4 throughout. Almost every active SMART deployment runs on it, and the tooling below targets it by default.
You’ll also want to know the difference between how you develop and how you ship. Locally, you can run everything over plain HTTP on localhost, since browsers and test servers don’t enforce TLS for loopback addresses. Production is a different story: every redirect URI and every FHIR endpoint has to run over HTTPS, no exceptions.
Here’s the minimum setup:
- A static file server for local testing, such as
npx http-serveror Vite’s dev server. - A client library. The SMART JavaScript client, fhirclient, is the fastest path for browser apps. Python, .NET, and Swift each have their own maintained clients if you’re building server-side or native apps.
- Sandbox access. SMART’s App Launcher is the standard starting point, and Logica Health runs a free public sandbox with realistic FHIR data if you want a second test environment.
- A text editor and a browser with dev tools open, since you’ll be reading network requests constantly.
How Does the SMART on FHIR Launch Flow Actually Work?
Every SMART app follows the same three phases, regardless of which EHR it eventually connects to.
Discovery comes first. Your app fetches the .well-known/smart-configuration document from the FHIR server’s base URL, and that JSON tells you everything about how the server behaves: the authorization endpoint, the token endpoint, which scopes it supports, and which client authentication methods it accepts, including private_key_jwt for backend clients. Skipping this step and hardcoding endpoints is the single most common reason integrations break when they move between sandbox and production.
Authorization comes next, and it runs on OAuth 2.0 with PKCE (Proof Key for Code Exchange). Your app redirects the user to the authorization endpoint with a client ID, a set of scopes, a redirect URI, and a code challenge. The user authenticates and consents, then the EHR redirects back with an authorization code.
Data access is the payoff. Your app exchanges that code for a bearer token, then attaches it to standard FHIR REST calls against endpoints like Patient/{id} or Observation?patient={id}&category=vital-signs. The SMART App Launch specification documents all three phases in detail, including the backend services variant for machine-to-machine access.

Which SMART Launch Pattern Fits Your App?
Not every SMART app launches the same way, and picking the wrong pattern early costs you a rebuild later.
EHR launch is what happens when a clinician clicks your app from inside the chart. The EHR redirects to your launch.html with an iss parameter (the FHIR server’s base URL) and a launch token. Since the EHR already knows which patient and encounter are active, you don’t need a patient picker. Request the launch scope alongside whatever clinical scopes you need, like patient/Observation.rs.
Standalone launch is the opposite: your app starts outside the EHR, so the user has to authenticate directly and pick a patient during consent. Request launch/patient instead of launch, since there’s no existing session context to inherit.
Backend services cover machine-to-machine access with no user in the loop at all, like nightly batch jobs pulling population-level data. These use private_key_jwt authentication instead of PKCE. Your app signs a JWT assertion with a private key, publishes the matching public key at a JWKS URL, and the server verifies the signature instead of checking a user session. This pattern matters most for bulk data exports and background analytics jobs.

Building a Minimal Browser SMART on FHIR App: Step by Step
Here’s a working file structure for a browser-based app, plus the code that ties it together.
-
Create two HTML files.
launch.htmlhandles the EHR launch entry point, andindex.htmlhandles everything after authorization succeeds. Keeping them separate matches the pattern used in SMART’s own tutorials and avoids tangled state between the pre-auth and post-auth phases. -
Load fhirclient from a CDN in both files. A single script tag gives you a global
FHIRobject with everything you need for authorization and requests. -
Kick off authorization in launch.html. Call
FHIR.oauth2.authorize()with your client ID, requested scopes, and redirect URI:
FHIR.oauth2.authorize({
clientId: "your-client-id",
scope: "launch patient/Observation.rs patient/Patient.rs",
redirectUri: "index.html",
iss: undefined // populated automatically from launch context
});
-
Let fhirclient handle PKCE for you. Behind the scenes, it generates a
code_verifier, computes the SHA256code_challenge, and stores the verifier in session storage so it survives the redirect. You don’t have to write any of that logic by hand. -
Retrieve the ready client in index.html. Once the EHR redirects back with an authorization code, call
FHIR.oauth2.ready(). It exchanges the code for a token and hands you a configured client object.
FHIR.oauth2.ready().then(function(client) {
client.patient.read().then(function(patient) {
console.log(patient);
});
client.request(
"Observation?patient=" + client.patient.id + "&category=vital-signs"
).then(function(bundle) {
console.log(bundle);
});
});
- Test against a sandbox before touching anything real. Point your app at SMART’s App Launcher, select a demo patient, and paste your
launch.htmlURL into the launcher’s app field. You can also prototype directly against an open, unprotected FHIR server in debugging mode before wiring up real OAuth.
Pro Tip: Keep your browser’s network tab open during every test run. The discovery response, the authorization redirect, and the token exchange all show up there, and reading them beats guessing when something silently fails.
Getting PKCE, Token Exchange, and Scopes Right
PKCE exists to stop authorization codes from being intercepted and replayed, and getting the mechanics wrong is one of the fastest ways to break a launch.
- Generate a random
code_verifier, then compute its SHA256 hash to produce thecode_challenge. Sendcode_challenge_method=S256in the authorization request so the server knows how to verify it later. - When you exchange the code for a token, your POST body needs the authorization code, your redirect URI, your client ID, and the original
code_verifier. The server checks that the verifier hashes back to the challenge you sent earlier. - The token response gives you an
access_token, often anid_token, and patient context fields likepatientdirectly in the JSON, so you don’t need a separate call just to learn which patient is active. - Scope syntax changed between SMART versions. Version 1 uses
.read(patient/Observation.read), while version 2 uses the more granular.rs(read + search) syntax alongside apermission-v2capability flag you can check in the discovery document. - Request
offline_accessonly if you genuinely need a refresh token for long-running background access. Most user-facing launches don’t need it, since the session naturally ends when the user closes the app.
Which Client Libraries and Sandboxes Should You Use?
Hand-rolling OAuth and raw FHIR requests is a common early mistake. It’s slow, and it’s easy to get subtly wrong in ways that only surface in production.
- fhirclient handles PKCE generation, token storage, token refresh, and exposes context helpers like
client.patient.idso you’re not parsing token payloads by hand. - Other ecosystems have equivalents. The SMART client-py library covers Python server-side apps, and there are actively maintained clients for .NET and Swift if you’re building native or backend integrations.
- Layer your testing. Start against an open FHIR server with no auth to validate your data calls, then move to the SMART App Launcher to emulate a real EHR launch, and only then register for a vendor-specific sandbox like Epic’s or Cerner’s developer portal.
If you’re building for imaging workflows specifically, integrations that pull in teleradiology overflow and backlog data often follow this exact same sandbox-first sequence before touching a live PACS-adjacent feed.
Your Production Checklist for SMART on FHIR Registration
Moving from sandbox to production is where most SMART on FHIR development guide advice gets skipped, and it’s where review failures pile up.
- Register separately for every EHR and every environment. A sandbox client ID and a production client ID are never interchangeable, even for the same vendor.
- Get your redirect URIs exact. Production requires HTTPS, and most EHRs reject even a trailing slash mismatch, so copy the URI character for character into your registration form.
- Publish a JWKS URL for backend services. If you’re using
private_key_jwt, the EHR needs a stable, publicly reachable endpoint serving your public keys, plus a documented key rotation policy. - Declare every scope you’ll ever request, up front. Requesting a scope at runtime that wasn’t declared at registration will typically get rejected outright, so list your full scope set before you submit for review.
- Build operational monitoring before launch. Track token expiration, handle revocation gracefully, and put key rotation on a calendar instead of leaving it as a someday task.
Common SMART on FHIR Errors and How to Fix Them
Most SMART integration failures trace back to a handful of repeat offenders.
- A redirect URI mismatch throws an opaque error on the authorization server’s side. Check the exact string you registered against the exact string your app sends, including protocol and trailing characters.
- Missing or wrong scopes show up as permission errors on data calls that otherwise look correct. Cross-check your requested scopes against the discovery document’s supported list.
- Manual FHIR URL concatenation breaks on edge cases like unencoded special characters and paginated bundles. Let your client library build the URL instead of using string concatenation.
- CORS failures and embedded webview quirks are common inside EHR-hosted browser frames. Reproduce them in the SMART App Launcher first, since it mimics EHR embedding far better than a plain browser tab.
Pro Tip: When a launch fails silently, check the discovery document first. Half the time the server simply doesn’t support the scope or auth method you assumed it did.
What I’ve Learned Building SMART Integrations
Every successful SMART project I’ve watched come together started the same way: discovery first, sandbox second, registration dead last. Teams that try to register with an EHR before their flow works cleanly in a sandbox end up burning review cycles on problems they could have caught locally in minutes.
The other recurring failure point is ownership. Someone needs to own per-EHR registration and JWKS key rotation as an ongoing job, not a one-time setup task, or you end up with an expired key breaking a backend integration nobody’s watching.
If your team is pulling data from a self-hosted or on-prem FHIR source and needs the resulting patient data de-identified before it touches an AI pipeline, that’s a narrower problem than general SMART tutorials cover. Medscrub’s developer tools approach it from the PHI-safety side rather than the OAuth side, which is worth knowing about even if it’s not the tool for every SMART project.
— Clint
An Alternative Path: PHI-Safe FHIR Access With Medscrub
Building your own SMART app gives you full control, but it also means you own every piece of the PHI handling yourself, from token storage to de-identification logic. If your team is building clinical AI features and want that layer handled instead of hand-built, Medscrub offers a developer-facing alternative: a self-hosted API with on-device anonymization and PHI de-identification, providing secure FHIR access while protecting patient data privacy.

This fits teams building AI-assisted clinical tools who need FHIR data flowing in without owning the full PHI compliance burden themselves, and it fits integrations where a traditional SMART app’s OAuth layer is only half the problem. If that’s closer to what you’re building, check out Medscrub’s developer offering and see whether the API fits your stack before you commit to building the PHI layer from scratch.
Where to Go Deeper on SMART on FHIR
For normative detail beyond this tutorial, start with the SMART App Launch implementation guide and its accompanying example payloads. The SMART Health IT server quick-start tutorial walks through sandbox testing in more depth, and the fhirclient documentation covers every method the JavaScript library exposes. For sandbox testing itself, SMART’s App Launcher remains the standard starting point.


