Architecture Overview
The SMART on FHIR Client is a five-layer Spring Boot application. Each layer has a single responsibility and communicates only with adjacent layers.
System diagramโ
Epic EHR :443 SMART Client :8080 HAPI FHIR
โ โ โ
โ 1. Clinician clicks โ โ
โ "Launch App" โ โ
โ โ โ
โโโ GET /launch?iss=...&launch= โโโโโบโ โ
โ SmartLaunchController โ
โ SmartDiscoveryService โโGET /.wellโโโโโโบ โ
โ (cache ISS, 10 min TTL) known/smart โ
โ PkceHelper (96-byte S256 verifier) โ
โ โ โ
โโโโ 302 /oauth2/authorize? โโโโโโโโโโ โ
โ code_challenge=... โ โ
โ โ โ
โ 2. Patient/clinician approves โ โ
โโโ GET /callback?code=... โโโโโโโโโโบโ โ
โ SmartCallbackController โ
โ SmartTokenService โโโPOST /tokenโโโโโบ โ
โ SmartContextExtractor โ
โ IdTokenValidator โ
โ (RS256, iss, aud, nonce, JWKS) โ
โโโโ 302 /dashboard โโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ 3. App makes FHIR requests โ โ
โ UiController / PatientDataController โ
โ FhirClientFactory โ
โ BearerTokenInterceptor โ
โ โโโGET /fhir/Patient/{id}โโโโโโโโบโ
โ โโโ Patient JSON โโโโโโโโโโโโโโโโโ
โ โโโGET /fhir/Condition?patientโโโบโ
โ โโโ Bundle (Conditions) โโโโโโโโโโ
โ โ โ
โ 4. Background token refresh โ โ
โ TokenRefreshFilter โ
โ (fires 120 s before exp on /api/**) โ
โ TokenRefreshService โโโPOST /tokenโโบ โ
Five layersโ
| Layer | Components | Responsibility |
|---|---|---|
| 5 โ UI / API | UiController, PatientDataController, Thymeleaf templates | Render views and return JSON |
| 4 โ Business Logic | SmartLaunchController, SmartCallbackController, SmartTokenService, SmartContextExtractor, IdTokenValidator | SMART handshake, token exchange, OIDC validation |
| 3 โ Security | SecurityConfig, SmartSecurityFilter, TokenRefreshFilter, PkceHelper | Filter chain, CSRF, PKCE, proactive refresh |
| 2 โ FHIR Client | FhirClientFactory, BearerTokenInterceptor | HAPI R4 client per ISS |
| 1 โ Session State | SmartLaunchContext, SmartLaunchSession, PkceParameters, UserProfile | Typed, serializable session objects |
Request lifecycleโ
Every EHR launch follows this exact 4-phase sequence:
Phase 1 โ Launch (GET /launch?iss=...&launch=...)
SmartLaunchController receives iss (the FHIR server URL) and launch (Epic's opaque context token). It calls SmartDiscoveryService.discover(iss) to fetch /.well-known/smart-configuration (cached per ISS for 10 minutes in Caffeine), generates a 96-byte PKCE code verifier with S256 challenge, stores PkceParameters in the HTTP session, and issues a 302 redirect to Epic's authorization endpoint.
Phase 2 โ Callback (GET /callback?code=...&state=...)
SmartCallbackController validates the CSRF state in constant time, exchanges the authorization code for tokens via SmartTokenService.exchangeCode(), validates the id_token RS256 signature and claims via IdTokenValidator, extracts SmartLaunchContext including patient ID, encounter ID, and needPatientBanner flag via SmartContextExtractor, and stores the context in the HTTP session. 302 redirect to /dashboard.
Phase 3 โ FHIR requests
FhirClientFactory.forContext(ctx) returns a HAPI IGenericClient configured with a BearerTokenInterceptor carrying the current access token and pointing at the patient's FHIR server. Every controller endpoint uses this client to make typed FHIR calls.
Phase 4 โ Token refresh
TokenRefreshFilter intercepts every /api/** request. TokenRefreshService.refreshIfNeeded() checks the remaining token lifetime and triggers a proactive refresh if less than 120 seconds remain. This runs before the controller method executes, so FHIR calls always use a valid token.
Next: Package Structure โ