Skip to main content

EHR Import

ChartChat imports patient health records from external EHR systems using SMART on FHIR (OAuth2 + PKCE). Once connected, the server periodically syncs FHIR resources from the EHR into ChartChat's FHIR store.

Files: packages/server/src/ehr-import/


Supported EHR Providers​

KeyEHRAuth method
epicEpic MyChartPKCE (form-encoded token request)
healoweClinicalWorks / HealowPKCE (form-encoded token request)
meditechMEDITECH ExpanseClient secret + JSON token request

In addition to these built-in providers, admins can configure custom Hospital Integrations that use dynamic SMART discovery to connect to any SMART on FHIR endpoint.


OAuth2 / PKCE Flow​

1. Patient initiates connection​

The Member Portal calls one of:

  • POST /fhir/ehr-import/start β€” for built-in EHR providers (epic, healow, meditech)
  • POST /fhir/ehr-import/hospital-start β€” for a configured HospitalIntegration (uses dynamic SMART discovery)

Both endpoints:

  1. Authenticate the request (patient bearer token required).
  2. Verify the caller is a Patient.
  3. Generate a PKCE pair (codeVerifier + codeChallenge).
  4. Create a short-lived OAuth state record in Redis (keyed by a UUID).
  5. Return { authorizeUrl } β€” the full EHR authorization URL including all OAuth params.

2. Browser redirects to EHR​

The Member Portal redirects the browser to authorizeUrl. The patient authenticates with the EHR and grants consent.

3. EHR redirects back to ChartChat​

The EHR sends the browser to GET /fhir/ehr-import/callback?code=...&state=....

The callback handler:

  1. Validates the state UUID and reads the stored OAuth state from Redis (single-use β€” consumed immediately).
  2. Exchanges the authorization code for tokens via the EHR's token endpoint.
  3. Extracts the external patient ID from the token response (or by calling GET {fhirBaseUrl}/Patient).
  4. Checks for conflicts β€” blocks if the EHR account is already linked to a different patient.
  5. Creates or updates a PatientEHRConnection resource with encrypted tokens.
  6. Caches the raw token in Redis (ehr:token:{connectionId}) for fast sync access.
  7. Triggers background FHIR sync (non-blocking).
  8. Redirects the browser back to the Member Portal (FRONTEND_URL).

On any error, the browser is redirected to FRONTEND_URL?ehr_error=<message>.


Token Security​

Access tokens and refresh tokens are encrypted before being stored in the FHIR database.

File: ehr-import/token-crypto.ts

  • Encryption uses EHR_TOKEN_ENCRYPTION_KEY from the server's .env.
  • Tokens are decrypted in memory only when needed for API calls.
  • Redis cache stores the raw token temporarily (TTL = token expiry time) to avoid repeated database reads during sync.

FHIR Sync​

Synced Resource Types​

When an EHR connection is established or refreshed, the server syncs the following FHIR resource types:

Resource type
Condition
AllergyIntolerance
MedicationRequest
Observation
Appointment
CarePlan
Encounter
Immunization
DiagnosticReport
DocumentReference
Procedure

Sync Process​

File: ehr-import/sync-service.ts, smart/utils/sync-patient.ts

  1. For each active PatientEHRConnection:
    • If a refresh token is available, proactively refresh the access token before syncing.
    • If the token is expired and there is no refresh token, mark the connection token_expired and skip.
  2. For each resource type, query the EHR's FHIR API using the patient's external ID.
  3. Upsert each resource into ChartChat's FHIR store using the internal patient reference.
  4. Resource types are processed in batches of 3 with a 1-second delay between batches to avoid overwhelming the EHR server.
  5. After sync completes, trigger an embed sync (triggerEmbedSync) to re-index the patient's records for the AI chat assistant (see Embed Sync below).

Sync Outcome States​

OutcomeMeaning
syncedAll resource types fetched and stored successfully
token_expiredToken expired and no refresh token available β€” patient must reconnect
errorNetwork error or unexpected failure

Redis Sync State​

During and after sync, status is written to Redis:

KeyValueTTL
ehr:sync:status:{patientId}syncing or idle5 minutes
ehr:sync:result:{patientId}JSON with syncedProviders, expiredProviders, errorProviders5 minutes
ehr:token:{connectionId}Raw token JSONToken expiry

The 5-minute TTL on the status key is a safety net β€” if the server crashes mid-sync, the status clears automatically so the next sync attempt is not blocked.


Disconnecting​

POST /fhir/ehr-import/disconnect/:connectionId

  1. Verifies the caller is the owner of the connection.
  2. Sets PatientEHRConnection.status = revoked.
  3. Deletes the Redis token cache entry.
  4. Writes an audit log event.

Hospital Integrations​

Hospital integrations allow admins to configure custom EHR endpoints without code changes. The flow uses dynamic SMART discovery (smart/smart-discovery.ts) to find the authorization_endpoint and token_endpoint at runtime from the hospital's {fhirBaseUrl}/.well-known/smart-configuration.

This allows connecting to any SMART on FHIR–compliant system regardless of the EHR vendor, as long as it supports the configured vendor's client credentials.



Embed Sync​

File: ehr-import/embed-sync.ts

After a successful EHR sync (or when a patient's FHIR data is created/updated), the server triggers a re-index of that patient's records in the AI embedding service. This keeps the AI Chat Assistant's context up to date with the latest health data.

Endpoint called​

POST {EMBED_API_URL}/api/embed/{patientId}/sync
PartDescription
EMBED_API_URLBase URL of the embedding/AI service. Set via env var. Defaults to https://dev-chatapi.chartchathealth.com if not set.
patientIdInternal ChartChat Patient resource ID

This call is fire-and-forget β€” the server does not wait for a response. Errors are logged but do not fail the sync or the request.

When it is triggered​

EventTrigger location
After EHR OAuth callback (new connection)ehr-import/routes.ts
After EHR background sync completesehr-import/sync-service.ts

Ingestion Logging​

File: smart/utils/sync-patient.ts

Every individual FHIR resource received from an EHR during sync is recorded as an EhrIngestionLog resource. This is a custom FHIR resource type defined in packages/fhirtypes/dist/EhrIngestionLog.d.ts.

EhrIngestionLog fields​

FieldTypeDescription
ehrstringEHR provider name (e.g. epic, healow, meditech) or Endpoint UUID for hospital integrations
sourceResourceTypestringFHIR resource type received (e.g. Condition, Observation)
statusstringsuccess or failed
syncBatchstringUUID grouping all resources ingested in the same sync run
receivedAtstringISO timestamp when the resource was received
patientReferenceReference to the internal Patient resource
connectionReferenceReference to the PatientEHRConnection (optional β€” present when connection ID is known)
storedResourceReferenceReference to the FHIR resource stored in ChartChat (null if ingestion failed)
payloadHashstringSHA-256 hash of the raw payload for deduplication
ehrData.payloadunknownRaw JSON payload as received from the EHR
ehrData.hashstringHash of the raw payload
source.resourceIdstringResource ID as assigned by the EHR
source.endpointstringFull EHR endpoint URL (e.g. https://fhir.epic.org/.../Condition)
outcome.error.messagestringError message if ingestion failed
timing.processedAtstringISO timestamp when processing completed
timing.durationMsnumberTime taken to process this single resource in milliseconds

When a log is created​

One EhrIngestionLog is created per resource per sync run. If syncing a patient with 3 Conditions and 5 Observations, 8 log entries are created for that batch β€” each with the same syncBatch UUID.

The connection field is populated only when the sync is triggered from a known PatientEHRConnection (i.e. most normal syncs). It is optional to handle edge cases where the connection ID is not available.

Search parameter​

EhrIngestionLog has a custom search parameter connection (defined in packages/definitions/) that allows querying logs by PatientEHRConnection reference:

GET /fhir/R4/EhrIngestionLog?connection=PatientEHRConnection/{id}

This is what the Admin Portal EHR Ingestion Log page uses to load all logs for a selected connection.


Audit Logging​

Every EHR connect and disconnect event is written as an AuditEvent resource via audit/audit-logger.ts. Failed callback attempts are also logged even if the patient ID is not available at the time of failure.