Teams new to Epic integration often expect the hard part to be the FHIR API itself, the resources, the JSON, the endpoints. In practice, the FHIR surface is the easy part; FHIR's request/response mechanics are the same REST patterns used elsewhere. What actually makes an Epic SMART on FHIR integration difficult is that authentication, launch context, scope approval, clinical terminology, write-back rules, testing against realistic data, and Epic's own customer-by-customer onboarding process all have to come together correctly at once. For teams building or evaluating FHIR integration services, these surrounding requirements are often where the real engineering complexity lies. This article walks through the architecture and the lessons that come out of getting each of those pieces right, based on an internal engineering demonstration against the Epic sandbox, clearly labeled as such, not a production client deployment.
What a SMART on FHIR integration actually includes
A working integration is made up of several distinct pieces, each with its own failure modes: the healthcare application itself, the EHR's authorization server, the FHIR server, the launch context that tells the app which patient and encounter are in focus, the access scopes granted to the app, a frontend, a backend, session handling, a layer that normalizes vendor-specific data into something the application can use consistently, and audit/monitoring covering what the app accessed and when.
SMART App Launch defines two launch models that behave differently enough to matter architecturally: a standalone launch, where the app starts on its own and the user selects a patient inside the app, and an EHR launch, where the app is opened from inside an active EHR session and receives a launch token identifying the patient and context already in view. Which one you build for changes how your app initializes, what context it can assume on load, and how it should behave if that context is missing.
Reference architecture
A practical architecture for this kind of integration, and the one used in our engineering demonstration, pairs a React frontend with a Node.js Backend-for-Frontend (BFF), server-side sessions, the Epic sandbox as the FHIR source, a normalization layer that reshapes Epic's FHIR responses into a stable internal model, an optional cache for non-clinical reference data, and audit logging.
React frontend → Node.js BFF (session store, token handling, normalization layer, audit logging) → Epic FHIR sandbox (OAuth 2.0 + FHIR R4 endpoints).
A BFF earns its place here for a few concrete reasons: it keeps access and refresh tokens off the browser, gives you one place to enforce session and backend-level authorization checks, lets you normalize Epic's responses before the frontend ever sees them, centralizes logging and error handling, and abstracts the frontend from vendor-specific API quirks, useful if you ever need to support a second EHR vendor behind the same frontend. None of that makes a BFF mandatory for every SMART application; a simpler standalone app with modest scope requirements can sometimes get away with a thinner backend. It's a design decision to make deliberately, not a default to skip.
SMART discovery
Before authenticating, a well-behaved SMART client fetches the EHR's .well-known/smart-configuration document, which, per the SMART App Launch specification, publishes the authorization endpoint, the token endpoint, supported capabilities, and available scopes for that specific environment. Because each Epic customer runs its own FHIR endpoint with its own configuration, discovering these values at runtime rather than hardcoding them is what lets the same application code work across a sandbox and multiple customer environments without a redeploy every time an endpoint changes. Hardcoding endpoints is a common shortcut early in a project that becomes a real liability the moment a second Epic organization enters the picture.
OAuth 2.0 authorization code with PKCE
SMART on FHIR authorizes public clients (like a browser-based or mobile app) using the OAuth 2.0 authorization code flow with PKCE. The sequence looks like this:
-
Generate a random state value to protect against cross-site request forgery.
-
Generate a PKCE code_verifier.
-
Derive the code_challenge from the verifier.
-
Redirect the user to the authorization endpoint with the challenge, state, and requested scopes.
-
The user authenticates and authorizes the app's requested access.
-
The app receives an authorization code at its redirect URI.
-
The app validates that the returned state matches what it generated.
-
The app exchanges the code, along with the original verifier, for an access token.
-
The application establishes a session for the authenticated user/context.
-
The app calls FHIR resources permitted by the granted scopes.
Most of the implementation pain shows up in a handful of recurring failure points: an invalid or mismatched redirect URI, a PKCE verifier lost across a redirect (common in mobile web views or if it's stored somewhere that doesn't survive the round trip), a state value that doesn't match on return, requesting scopes the environment doesn't actually grant, an authorization code that's expired by the time it's exchanged, tokens leaking into logs or client-side storage, and session handling that doesn't cleanly separate one user's context from another's. None of these are exotic bugs, they're the standard OAuth implementation mistakes, just with more consequence given the data involved.
Scopes and least privilege
Request only the scopes the application actually needs. SMART scopes combine a context (patient/ or user/), a resource type, and an access level (.read or .write), alongside separate launch scopes that grant context like launch/patient. Broader scope requests are more likely to draw scrutiny during Epic's review process and increase the blast radius if a token is ever compromised.
What matters most here: scope support and approval vary by application and by Epic customer environment. A scope that's available and pre-approved in the public sandbox is not automatically available in a specific hospital's production environment, that's a separate approval conversation with each customer, and sandbox access should never be treated as a preview of guaranteed production scope.
FHIR resources and clinical meaning
The table below uses the resource set from our engineering demonstration as a representative, not exhaustive, example of how a small set of FHIR resources maps to product use and to the normalization work each one tends to require.
Representative FHIR resources and their product/normalization considerations
| Resource | Product use | Relevant terminology | Common normalization challenge |
|---|---|---|---|
| Patient | Identity, demographics, patient banner | Identifier systems | Multiple identifiers across systems; missing fields |
| Condition | Problem list, diagnosis history | ICD-10-CM, SNOMED CT | Mixed coding systems; inconsistent display text |
| Observation | Vitals, lab results, trends | LOINC, units of measure (UCUM) | Unit variation; duplicate or superseded readings |
| MedicationRequest | Active and past medications | RxNorm | Free-text vs coded dosage; status semantics |
| AllergyIntolerance | Allergy and reaction display | RxNorm, SNOMED CT | Severity and reaction coding gaps |
| Encounter | Visit history, context for other resources | Encounter class/type codes | Linking related resources back to the right encounter |
| DocumentReference | Clinical notes, attachments | Document type codes (LOINC) | Binary content handling; inconsistent metadata |
Clinical terminology
Standards-based transport does not eliminate the need for terminology normalization. Getting a Condition resource over FHIR doesn't mean you've gotten a clean diagnosis, the underlying code might be ICD-10-CM, SNOMED CT, a local code, or missing entirely with only display text to fall back on. The same applies to Observation units of measure, MedicationRequest coding via RxNorm, and lab results via LOINC, each has its own versioning, and display text doesn't always match the coded value cleanly.
Handling this well means building explicit terminology mapping and fallback logic rather than assuming every resource arrives fully coded, a topic we go deeper on in our healthcare terminology services article.
Resource normalization
Frontend applications generally shouldn't consume raw, vendor-specific FHIR bundles directly everywhere in the UI. A normalization layer that reshapes Epic's responses into a stable internal model has to account for optional fields that may or may not be present, extensions carrying vendor-specific data, references between resources, pagination and bundle structure, inconsistent date formats, arrays of codings where only one is clinically relevant, null values, and display text that doesn't always agree with the coded value.
There's a real tradeoff here. Normalization makes the frontend simpler to build and more stable against upstream changes, but over-abstracting can hide clinically important detail a clinician-facing view actually needs to surface, like which specific coding system a diagnosis came from. The right amount of normalization depends on who's consuming the data and what they need to trust about it.
Observation read and write workflows
Read considerations
- Search parameters (patient, code, date)
- Patient context scoping
- Code-based filtering
- Date-range filtering
- Pagination across large result sets
- Unit consistency
- Duplicate observations across sources
Write considerations
- Write permissions actually granted
- Required profiles and conformance
- Identifier management
- Correct status values
- Correct coding and units
- Provenance tracking
- Server-side validation responses
- Duplicate-write prevention
- Audit history for the write
The most important lesson here: sandbox write success does not guarantee production write approval. Write access into a live EHR is a significantly bigger trust decision for a healthcare organization than read access, and it's typically gated by its own review, contractual, and clinical-governance process separate from read-only sandbox testing.
Error handling
A production-ready integration needs to handle authentication errors, authorization errors, insufficient-scope responses, expired tokens, temporarily unavailable endpoints, invalid search parameters, FHIR's own OperationOutcome resource for structured error detail, validation failures on write, rate limits where they apply, timeouts, partial or incomplete bundle data, and missing references between resources.
User-facing error messages should not be the same as engineering logs. A clinician or patient needs a clear, calm explanation of what to do next; your logs need the actual OperationOutcome detail, the request context, and enough information to debug without ever including PHI in a log line that isn't properly access-controlled.
Caching
Limited caching can genuinely help in a few places: terminology lookups, non-volatile reference data, short-lived application views, and repeated calls against a slow sandbox during development. It also introduces real risk: stale clinical data being shown as current, PHI ending up in a cache layer that wasn't designed to hold it, cross-tenant data leakage in a multi-tenant deployment, unclear invalidation rules, tokens cached somewhere they shouldn't be, and audit gaps where a cached response bypasses the logging a live call would have triggered. There's no universal safe TTL to reach for, cache lifetime has to be set per data type, weighed against how stale that specific type of data is allowed to be.
Testing strategy
A reasonably complete test strategy covers unit tests, mocked FHIR responses, integration tests against the sandbox, SMART callback and redirect tests, deliberately malformed resources, resources missing optional fields, pagination edge cases, terminology mapping and fallback behavior, authorization-failure paths, write-validation failures, and end-to-end browser tests, all while accounting for the sandbox's own availability limitations, which are real and worth planning around.
Recorded fixtures used in tests must be privacy-safe, synthetic or properly de-identified data, never captured PHI. And a high coverage percentage on its own doesn't guarantee integration quality; it's entirely possible to have thorough coverage of the happy path and thin coverage of the edge cases, expired tokens, malformed bundles, missing terminology, that actually determine whether the integration holds up against a real Epic environment.
CI/CD and environment management
Sandbox and production need to be genuinely separate: distinct environment variables, proper secret management, separately registered and managed redirect URIs per environment, automated tests running in CI, dependency scanning, a deliberate deployment-approval step before anything touches a customer's production Epic environment, a tested rollback path, and observability that actually surfaces authentication and integration failures rather than just infrastructure metrics. None of this is unique to healthcare, but the cost of getting it wrong, an exposed token, a redirect URI misconfiguration in production, is higher when PHI is on the other end of the connection.
Sandbox vs. production
This is worth treating as its own decision point rather than an afterthought, the two environments differ on nearly every dimension that matters for planning.
Epic sandbox vs. production environment, what actually changes
| Dimension | Sandbox | Production |
|---|---|---|
| Data | Synthetic test patients | Real patient data |
| Users | Test accounts | Real clinicians and patients |
| App registration | Self-service, non-production client ID | Requires a separate production registration and client ID |
| Customer involvement | None, public sandbox | Each Epic customer organization must individually approve and onboard the app |
| Scopes | Broadly available for testing | Approved per customer, often narrower than sandbox |
| Security review | Minimal | Formal review by Epic and/or the customer organization |
| Contractual requirements | None beyond developer terms | Agreements with Epic and/or the customer organization |
| Performance | Not representative of production load | Real-world latency and load patterns |
| Monitoring | Optional, low stakes | Required, operational visibility into a live clinical system |
| Support | Community/developer forums | Formal support channel per customer relationship |
| Write access | Often permissive for testing | Independently reviewed and frequently more restricted |
| Change management | Developer-controlled | Coordinated with the customer's change windows and governance |
Common implementation mistakes
Coding before confirming onboarding requirements. Fix: understand Epic and customer onboarding requirements before writing production-bound code.
Hardcoding authorization endpoints. Fix: use SMART discovery so the app adapts across environments.
Requesting excessive scopes. Fix: request only what the current feature set actually needs.
Exposing tokens to unnecessary layers. Fix: keep tokens in the backend/BFF, never in client-accessible storage.
Assuming every FHIR field is populated. Fix: design the UI and normalization layer to handle missing fields gracefully.
Skipping terminology handling. Fix: build explicit mapping and fallback logic for codes and units.
Treating sandbox data as representative. Fix: test against messier, more realistic data patterns before production.
Ignoring OperationOutcome. Fix: parse and surface structured error detail rather than a generic failure message.
No reconciliation workflow. Fix: build a way to detect and resolve data that doesn't match expectations after sync.
Assuming write access. Fix: confirm write scope and profile support per customer before building write features.
Logging PHI. Fix: scrub logs of patient-identifying detail and control access to what remains.
Treating Epic implementations as identical. Fix: expect configuration and scope differences across every customer environment.
Underestimating production onboarding. Fix: budget real time for customer-specific review, approval, and testing.
Read More: 10 FHIR Integration Architecture Mistakes That Delay HealthTech Products
Practical implementation roadmap
-
Define the product workflow the integration needs to support.
-
Identify the required FHIR resources for that workflow, no more.
-
Confirm the launch model, standalone, EHR-launched, or both.
-
Register a sandbox application with Epic and obtain a non-production client ID.
-
Implement authentication, discovery, PKCE, session handling.
-
Build read workflows against the confirmed resource set.
-
Normalize the data into a stable internal model.
-
Add a controlled write workflow, only if the product actually requires one.
-
Test errors and edge cases deliberately, not just the happy path.
-
Prepare customer-specific production onboarding with each Epic organization involved.
-
Deploy monitoring and operational support before go-live, not after.
We're intentionally not attaching universal timelines to these phases, onboarding timeline varies significantly by customer, use case, and scope, and a fixed number here would mislead more than it would help.
Engineering demonstration
See how Peerbits applied this architecture in an Epic sandbox engineering demonstration.
View the engineering demonstration. This was an internal engineering exercise against the public Epic sandbox, built to validate the architecture described in this article, it is not a production client deployment, and it should not be read as a claim of Epic certification or a live customer integration.
How Peerbits helps
Peerbits is a healthcare software engineering company that designs and builds Epic and EHR integrations using SMART on FHIR.
- Epic and EHR integration discovery
- SMART on FHIR architecture
- FHIR resource mapping
- Authentication and authorization implementation
- Backend integration and normalization
- Patient and clinician application development
- Write-back workflow design
- Terminology normalization
- Testing strategy
- Security engineering
- Production onboarding support
- Integration monitoring
- Healthcare product modernization
- Dedicated FHIR developers
Peerbits builds and integrates this software directly. We don't claim an official Epic partnership or certification unless independently verified for the specific engagement, and we're direct with clients about the difference between sandbox capability and confirmed production access with a given Epic customer.
Planning an Epic SMART on FHIR integration?
Tell us about your target workflow, required FHIR resources, launch model, and whether write-back is in scope, we'll help you architect it against what Epic and your specific customer environment actually support.
Discuss Your Epic IntegrationFrequently asked questions
SMART on FHIR is an HL7 implementation guide that layers OAuth 2.0-based authorization and app-launch context on top of the FHIR standard, allowing third-party applications to securely access EHR data with defined, scoped permissions.
Yes, Epic publishes FHIR R4 APIs through Epic on FHIR, alongside a public sandbox for development and testing. Specific resource and scope availability can vary by Epic customer environment.
An EHR launch starts from inside an active EHR session, with Epic passing launch context identifying the current patient. A standalone launch starts independently of the EHR, and the app is responsible for establishing patient context itself, typically after the user logs in directly.
OAuth 2.0 provides a standard, well-understood way to grant a third-party app scoped, revocable access to a user's data without sharing credentials directly with that app, which is exactly the access model healthcare data exchange needs.
PKCE (Proof Key for Code Exchange) is an OAuth 2.0 extension that protects the authorization code exchange for public clients, like browser-based or mobile apps, that can't securely store a traditional client secret.
In some cases, yes, if the app has been granted the appropriate write scopes and meets Epic's and the customer's requirements for profiles, validation, and provenance. Write access is reviewed independently of read access and is typically more restricted in production.
No. Sandbox access is largely self-service and permissive for development purposes. Production access requires a separate registration, customer-specific approval, and often a narrower set of granted scopes.
It depends on the app's registered scopes and the specific Epic customer's configuration. Commonly used resources include Patient, Condition, Observation, MedicationRequest, AllergyIntolerance, Encounter, and DocumentReference, among others Epic supports.
It varies significantly based on the workflow's complexity, whether write access is required, and each Epic customer's own onboarding and review timeline, there's no fixed universal timeline that applies across projects.
Peerbits helps with Epic and EHR integration discovery, SMART on FHIR architecture, authentication, FHIR resource mapping, terminology normalization, testing, and production onboarding support, working alongside your team and the Epic customer's own review process.








