Skip to main content

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โ€‹

LayerComponentsResponsibility
5 โ€” UI / APIUiController, PatientDataController, Thymeleaf templatesRender views and return JSON
4 โ€” Business LogicSmartLaunchController, SmartCallbackController, SmartTokenService, SmartContextExtractor, IdTokenValidatorSMART handshake, token exchange, OIDC validation
3 โ€” SecuritySecurityConfig, SmartSecurityFilter, TokenRefreshFilter, PkceHelperFilter chain, CSRF, PKCE, proactive refresh
2 โ€” FHIR ClientFhirClientFactory, BearerTokenInterceptorHAPI R4 client per ISS
1 โ€” Session StateSmartLaunchContext, SmartLaunchSession, PkceParameters, UserProfileTyped, 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 โ†’