Skip to main content

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

FilePurposeWhen active
application.ymlBase defaults — all environmentsAlways
application-smart.ymlSMART Health IT sandbox overrides-Dspring-boot.run.profiles=smart
application-epic.ymlEpic 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

RequiredYes — app refuses to start without it
Env varEPIC_CLIENT_ID
Defaultyour-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
Use environment variables

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
Different IDs for different environments

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

RequiredYes
Env varEPIC_REDIRECT_URI
Defaulthttp://localhost:8080/callback
Validation@NotBlank

The URI Epic redirects the browser to after the user authorises the app. This must:

  1. Exactly match one of the redirect URIs registered in your App Orchard entry (character-for-character)
  2. Be https:// in production — Epic rejects http:// 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
Exact match required

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

RequiredAt 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

ScopePurposeRequired for
launchBinds the EHR patient/encounter contextEHR launch (must be first)
openidEnables OIDC — id_token will be includedGET /api/me endpoint
fhirUserIncludes name and fhirUser in id_tokenClinician 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}
LetterOperation
rRead (individual resource by ID)
sSearch (query by parameters)
cCreate
uUpdate
dDelete

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
Scope gating in the UI

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

RequiredNo
Default60
Minimum1 (validated by @Min(1))
UnitMinutes

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.

Sandbox tip

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

Default30000 (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

Default10000 (10 seconds)

Maximum time to wait for the TCP connection to Epic's server to be established.

hapi.fhir.log-requests-and-responses

Defaultfalse
Env varHAPI_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.

Never enable in production

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

Default8080

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

Default30m
FormatSpring 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.

Session vs token expiry

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

ValueDescriptionUse when
nonePlain HttpSession — in-memory, not sharedSingle instance, development
redisSpring Session + Redis — shared across instancesProduction, load balanced
Switch to Redis for production

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:

  1. Register its own security filter chain that intercepts all OAuth2 flows
  2. Protect /launch and /callback before Epic can reach them
  3. 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
LoggerDefaultDescription
com.smartfhirDEBUGAll app classes — launch flow, token exchange, context extraction
org.springframework.securityINFOSecurity filter decisions — raise to DEBUG to trace auth issues
ca.uhn.fhirWARNHAPI FHIR library — raise to INFO to see request summaries
Debugging the SMART flow

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

VariablePropertyRequiredDescription
EPIC_CLIENT_IDsmart.epic.client-idYesNon-production or production Epic client ID
EPIC_REDIRECT_URIsmart.epic.redirect-uriNoDefaults to http://localhost:8080/callback
HAPI_LOG_REQUESTShapi.fhir.log-requests-and-responsesNoSet true for request logging
REDIS_HOSTspring.data.redis.hostProduction onlyRedis host for session clustering
REDIS_PORTspring.data.redis.portProduction onlyRedis port (default 6379)
REDIS_PASSWORDspring.data.redis.passwordProduction onlyRedis 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.