Configuration
The app is configured through Spring Boot's standard application.yml file with profile overrides
for each testing environment. This page covers every property, its default value, validation
constraints, and when you would change it.
Configuration files
| File | Purpose | When active |
|---|---|---|
application.yml | Base defaults — all environments | Always |
application-smart.yml | SMART Health IT sandbox overrides | -Dspring-boot.run.profiles=smart |
application-epic.yml | Epic non-production sandbox overrides | -Dspring-boot.run.profiles=epic |
Spring profiles layer on top of the base file — values in a profile override the base, while
anything not overridden falls through from application.yml.
SMART / Epic settings
The most important section. All properties are bound to EpicProperties
and validated at startup — the app refuses to start if required values are missing.
smart:
epic:
client-id: ${EPIC_CLIENT_ID:your-non-production-client-id}
redirect-uri: ${EPIC_REDIRECT_URI:http://localhost:8080/callback}
scopes:
- launch
- openid
- fhirUser
- patient/Patient.rs
- patient/Condition.rs
- patient/MedicationRequest.rs
discovery-cache-minutes: 60
smart.epic.client-id
| Required | Yes — app refuses to start without it |
| Env var | EPIC_CLIENT_ID |
| Default | your-non-production-client-id (placeholder only — not functional) |
| Validation | @NotBlank |
Your Epic application's OAuth2 client identifier. Epic issues two IDs when you register an app:
- Non-production — use this for all sandbox and development testing
- Production — use this only after App Orchard approval and for real patient data
Never commit a real client ID to source control. Use the EPIC_CLIENT_ID env var:
export EPIC_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
The non-production and production client IDs are different strings. Using the production ID against the sandbox does not work, and vice versa.
smart.epic.redirect-uri
| Required | Yes |
| Env var | EPIC_REDIRECT_URI |
| Default | http://localhost:8080/callback |
| Validation | @NotBlank |
The URI Epic redirects the browser to after the user authorises the app. This must:
- Exactly match one of the redirect URIs registered in your App Orchard entry (character-for-character)
- Be
https://in production — Epic rejectshttp://for non-localhost URIs in production
Development
smart:
epic:
redirect-uri: http://localhost:8080/callback
Production
smart:
epic:
redirect-uri: $\{EPIC_REDIRECT_URI\} # e.g. https://yourapp.hospital.com/callback
https://yourapp.com/callback and https://yourapp.com/callback/ (trailing slash)
are treated as different URIs by Epic. Register exactly the URI your app sends.
smart.epic.scopes
| Required | At least one scope |
| Default | [launch, openid, fhirUser, patient/Patient.rs, patient/Condition.rs, patient/MedicationRequest.rs] |
| Validation | @NotEmpty |
The OAuth2 scopes requested during the authorize redirect. Epic will only grant scopes that are registered for your app in App Orchard.
EHR launch scopes
| Scope | Purpose | Required for |
|---|---|---|
launch | Binds the EHR patient/encounter context | EHR launch (must be first) |
openid | Enables OIDC — id_token will be included | GET /api/me endpoint |
fhirUser | Includes name and fhirUser in id_token | Clinician display name |
Standalone launch scopes
For standalone launch, launch is automatically replaced
by launch/patient — the controller handles this transparently. You do not need to configure
a separate scope list for standalone mode.
FHIR resource scopes (SMART v2 syntax)
patient/{ResourceType}.{operations}
| Letter | Operation |
|---|---|
r | Read (individual resource by ID) |
s | Search (query by parameters) |
c | Create |
u | Update |
d | Delete |
So patient/Patient.rs means read and search for Patient resources in the patient's context.
Minimal (read only)
scopes:
- launch
- openid
- patient/Patient.rs
Standard clinical app
scopes:
- launch
- openid
- fhirUser
- patient/Patient.rs
- patient/Condition.rs
- patient/MedicationRequest.rs
Extended (add observations)
scopes:
- launch
- openid
- fhirUser
- patient/Patient.rs
- patient/Condition.rs
- patient/MedicationRequest.rs
- patient/Observation.rs
- patient/AllergyIntolerance.rs
Every GET /api/* endpoint calls SmartLaunchContext.hasScope() before making a FHIR call.
If a scope wasn't granted, the endpoint returns 403 with a clear message — you'll never get
a silent empty result because a scope was missing.
smart.epic.discovery-cache-minutes
| Required | No |
| Default | 60 |
| Minimum | 1 (validated by @Min(1)) |
| Unit | Minutes |
How long SmartDiscoveryService caches the .well-known/smart-configuration document per ISS
before re-fetching. Epic's discovery document rarely changes, so 60 minutes is a safe default.
The cache is keyed by normalised ISS URL — in a multi-tenant deployment where multiple hospitals launch the app, each hospital gets its own cache entry.
Use a shorter TTL in sandbox environments:
smart:
epic:
discovery-cache-minutes: 10
Epic occasionally updates sandbox configuration; a shorter TTL ensures you pick up changes faster.
HAPI FHIR client settings
Controls the HTTP connection pool used for all outbound FHIR API calls.
hapi:
fhir:
socket-timeout-ms: 30000
connect-timeout-ms: 10000
connection-request-timeout-ms: 10000
log-requests-and-responses: ${HAPI_LOG_REQUESTS:false}
hapi.fhir.socket-timeout-ms
| Default | 30000 (30 seconds) |
| Env var | — |
Maximum time to wait for data to arrive after a connection is established. Epic's FHIR API typically responds within 1–3 seconds for simple reads; complex searches or large bundles may take longer.
hapi.fhir.connect-timeout-ms
| Default | 10000 (10 seconds) |
Maximum time to wait for the TCP connection to Epic's server to be established.
hapi.fhir.log-requests-and-responses
| Default | false |
| Env var | HAPI_LOG_REQUESTS |
When true, logs the HTTP method, URL, status code, and summary for every FHIR call.
Response bodies are not logged even when this is enabled — FHIR responses contain PHI.
Even the request summaries (URL, status) reveal which FHIR resources are being accessed
and may constitute PHI in some audit contexts. Keep this false in production.
Development
hapi:
fhir:
log-requests-and-responses: true
Production
hapi:
fhir:
log-requests-and-responses: false
Server settings
server:
port: 8080
servlet:
session:
timeout: 30m
server.port
| Default | 8080 |
The port the embedded Tomcat server listens on. Change if 8080 is occupied or if your
deployment environment requires a different port (typically 443 or 8443 for HTTPS, though
TLS is usually terminated at a reverse proxy rather than in the app itself).
server.servlet.session.timeout
| Default | 30m |
| Format | Spring duration format (30m, 1h, 3600s) |
Maximum idle time before an HTTP session expires. When a session expires, the
SmartLaunchContext is lost and any /api/* request returns 401 — the clinician must
re-launch the app from the EHR.
This is the session timeout — the server-side session lifetime.
The token expiry (expiresAt in SmartLaunchContext) is controlled by Epic and is
typically 1 hour. TokenRefreshService refreshes the token proactively 120 seconds before
it expires, so in normal use the token is always fresh within an active session.
A session timeout of 30 minutes is appropriate for clinical apps where a clinician may close the tab and return within that window.
Session store settings
spring:
session:
store-type: none
spring.session.store-type
| Value | Description | Use when |
|---|---|---|
none | Plain HttpSession — in-memory, not shared | Single instance, development |
redis | Spring Session + Redis — shared across instances | Production, load balanced |
With store-type: none, sessions are lost if the app restarts or if a load balancer
routes a request to a different instance. For any production deployment behind a load
balancer, Redis is required.
All session objects (SmartLaunchSession, PkceParameters, SmartLaunchContext,
UserProfile) implement Serializable with pinned serialVersionUID — they are
ready for Redis serialisation out of the box.
# Production Redis session config
spring:
session:
store-type: redis
redis:
flush-mode: on-save
namespace: smart-fhir
data:
redis:
host: $\{REDIS_HOST:localhost\}
port: $\{REDIS_PORT:6379\}
password: $\{REDIS_PASSWORD:\}
Add to pom.xml:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
Spring Security auto-configuration
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration
This exclusion is required and must not be removed. Here's why:
The app includes spring-boot-starter-oauth2-client to get access to PKCE helpers and OAuth2
token client classes. However, Spring Boot's OAuth2 auto-configuration would, if active:
- Register its own security filter chain that intercepts all OAuth2 flows
- Protect
/launchand/callbackbefore Epic can reach them - Conflict with our manually-driven SMART flow
By excluding OAuth2ClientAutoConfiguration, we retain access to the OAuth2 classes we need
while keeping full control over the launch flow.
Actuator settings
management:
endpoints:
web:
exposure:
include: health, info
endpoint:
health:
show-details: when-authorized
Only health and info endpoints are exposed. GET /actuator/health is permitted by
SecurityConfig without authentication — used by load balancers and Epic's integration
monitoring. Health details (disk space, database status) are only shown to authenticated users.
Logging settings
logging:
level:
com.smartfhir: DEBUG
org.springframework.security: INFO
ca.uhn.fhir: WARN
| Logger | Default | Description |
|---|---|---|
com.smartfhir | DEBUG | All app classes — launch flow, token exchange, context extraction |
org.springframework.security | INFO | Security filter decisions — raise to DEBUG to trace auth issues |
ca.uhn.fhir | WARN | HAPI FHIR library — raise to INFO to see request summaries |
To trace the complete launch sequence in the logs:
logging:
level:
com.smartfhir: DEBUG
org.springframework.security: DEBUG
ca.uhn.fhir: INFO
You'll see each step: ISS received → discovery fetched → PKCE generated → authorize URL built → state validated → token exchanged → context stored.
Spring profiles
smart profile (SMART Health IT sandbox)
Activate with -Dspring-boot.run.profiles=smart. No credentials needed.
# application-smart.yml
smart:
epic:
client-id: growth_chart # public client ID accepted by launch.smarthealthit.org
redirect-uri: http://localhost:8080/callback
discovery-cache-minutes: 5
hapi:
fhir:
log-requests-and-responses: true
epic profile (Epic non-production sandbox)
Activate with -Dspring-boot.run.profiles=epic. Requires EPIC_CLIENT_ID env var.
# application-epic.yml
smart:
epic:
client-id: ${EPIC_CLIENT_ID} # required — no fallback
redirect-uri: http://localhost:8080/callback
discovery-cache-minutes: 10
logging:
level:
org.springframework.security: DEBUG
Environment variable reference
| Variable | Property | Required | Description |
|---|---|---|---|
EPIC_CLIENT_ID | smart.epic.client-id | Yes | Non-production or production Epic client ID |
EPIC_REDIRECT_URI | smart.epic.redirect-uri | No | Defaults to http://localhost:8080/callback |
HAPI_LOG_REQUESTS | hapi.fhir.log-requests-and-responses | No | Set true for request logging |
REDIS_HOST | spring.data.redis.host | Production only | Redis host for session clustering |
REDIS_PORT | spring.data.redis.port | Production only | Redis port (default 6379) |
REDIS_PASSWORD | spring.data.redis.password | Production only | Redis auth password |
Validation errors at startup
EpicProperties is validated with Bean Validation on startup. If required properties are
missing or invalid, the app exits immediately with a clear message:
APPLICATION FAILED TO START
Description:
Binding to target ... failed:
Field error in object 'smart.epic' on field 'clientId':
rejected value [your-non-production-client-id];
codes [NotBlank.smart.epic.clientId,...];
default message [smart.epic.client-id must be set (use EPIC_CLIENT_ID env var)]
This is intentional — failing fast with a clear message is better than starting up and crashing on the first launch request with a confusing OAuth2 error.