ehr-connect
v0.1.0
Published
Unified, Stripe-like developer experience for EHR and FHIR R4 integration
Maintainers
Readme
⚡ The Problem
Integrating with Electronic Health Record (EHR) systems like Epic, Oracle Health (Cerner), or Athena is notoriously difficult:
- Fragmented Auth: SMART Backend Services requires RFC 7523 asymmetric JWT assertions (
RS384for Epic,RS256for Oracle), JWKS hosting, and complex token caching. - Provider Quirks: Epic rejects Observation searches that lack an explicit
category; Oracle requires custom endpoint prefixes; Cerner handles pagination links differently than HAPI FHIR. - Messy FHIR Schemas: FHIR R4 is deeply nested and flexible. Extracting a patient's name, primary phone, blood pressure, or active medication requires navigating dozens of optional arrays, codeableConcepts, and extensions.
- Zero Unified Developer Experience: Developers have had to build custom OAuth flows, retry logic, secret redaction, and normalizers from scratch for every provider.
💡 The Solution: EHRConnect
ehr-connect gives developers a Stripe-like developer experience for healthcare data. One unified API connects to multiple EHR vendors with zero manual FHIR boilerplate.
npm install ehr-connectimport { createEHR } from "ehr-connect";
const ehr = createEHR({
provider: "epic",
environment: "sandbox",
credentials: {
clientId: process.env.EPIC_CLIENT_ID!,
privateKey: process.env.EPIC_PRIVATE_KEY! // PKCS#8 PEM or JWK
}
});
await ehr.connect();
// Normalized, clean TypeScript models across all EHRs
const patient = await ehr.patients.get("123");
console.log(patient.fullName, patient.gender, patient.birthDate);
const observations = await ehr.observations.list({ patientId: "123" });
const medications = await ehr.medications.list({ patientId: "123" });🚀 Key Features
| Feature | Description |
| :--- | :--- |
| Unified 11 Domain Services | Standardized APIs for patients, practitioners, encounters, observations, conditions, medications, allergies, procedures, diagnosticReports, documents, and appointments. |
| Normalized Data Layer | Deep FHIR resources mapped to clean, flat, strongly-typed TypeScript interfaces (fullName, primaryPhone, bloodPressure, status). Raw FHIR resource always preserved via raw. |
| Zero-Config Mock Provider | Out-of-the-box in-memory mock EHR with 10 realistic, clinically correlated synthetic patients for instant local dev & unit tests without credentials or Docker. |
| Production SMART-on-FHIR Engine | RFC 7523 Backend Services (private_key_jwt with RS384/RS256), Client Credentials, PKCE Authorization Code flow, token caching, and automatic renewal. |
| EHR Quirk Abstraction | Automatically remedies provider-specific bugs, such as Epic's Observation category requirement and custom auth endpoint URLs. |
| Raw FHIR Escape Hatch | Need vendor-specific extensions or raw FHIR operations? Use ehr.fhir.read(), search(), create(), patch(), transaction(), etc. directly. |
| Async Streaming Iterators | for await (const obs of ehr.observations.iterate({ patientId })) streams through multi-page Bundle results with transparent cursor pagination. |
| HIPAA & PHI Security | Built-in zero-password architecture, automatic redaction of secrets and JWTs in all logs and error objects, and exponential backoff retry with jitter. |
📦 Installation
# Using npm
npm install ehr-connect
# Using pnpm
pnpm add ehr-connect
# Using yarn
yarn add ehr-connect🏃 5-Minute Quickstart (Zero-Config Mock)
Run immediately without needing any API keys or network access:
import { createEHR } from "ehr-connect";
async function run() {
// 1. Initialize Mock Provider
const ehr = createEHR({ provider: "mock" });
await ehr.connect();
// 2. Fetch all 10 synthetic patients
const patients = await ehr.patients.list();
console.log(`Loaded ${patients.length} patients.`);
// 3. Inspect Patient #1 (John Michael Doe)
const patient = await ehr.patients.get("1");
console.log(`Patient: ${patient.fullName} (DOB: ${patient.birthDate})`);
console.log(`Primary Phone: ${patient.primaryPhone}`);
// 4. Retrieve Vital Signs
const vitals = await ehr.observations.list({
patientId: "1",
category: "vital-signs"
});
console.log(`Vitals:`, vitals.map(v => `${v.codeText}: ${v.value ?? v.valueString} ${v.unit ?? ""}`));
// 5. Clean up
await ehr.disconnect();
}
run().catch(console.error);🔌 Supported Providers
1. Epic Systems (provider: "epic")
Supports both the free open Epic Developer Sandbox and clinical production environments.
import { createEHR, generateRSAKeyPair } from "ehr-connect";
// Utility to generate RS384 keypair for Epic App registration
const keyPair = await generateRSAKeyPair(2048);
console.log("Paste this JWK into fhir.epic.com:", keyPair.jwks);
const ehr = createEHR({
provider: "epic",
environment: "sandbox", // Defaults to https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4
credentials: {
clientId: process.env.EPIC_CLIENT_ID!,
privateKey: process.env.EPIC_PRIVATE_KEY! // PKCS#8 PEM string
}
});
await ehr.connect();
const patient = await ehr.patients.get("erXuFYUfucBZ3GRGg1m0sw3"); // Epic Sandbox Test Patient2. Oracle Health / Cerner (provider: "oracle")
Supports Oracle Health Millennium Sandbox (code.cerner.com) and Production.
const ehr = createEHR({
provider: "oracle",
environment: "sandbox",
credentials: {
clientId: process.env.ORACLE_CLIENT_ID!,
clientSecret: process.env.ORACLE_CLIENT_SECRET // Or SMART private_key_jwt
}
});
await ehr.connect();
const encounters = await ehr.encounters.list({ patientId: "12724066" });3. Generic FHIR R4 (provider: "fhir")
Connect to any FHIR R4 server (e.g. HAPI FHIR, Google Cloud Healthcare API, Azure Health Data Services, AWS HealthLake, Smile CDR) with optional SMART discovery:
const ehr = createEHR({
provider: "fhir",
fhirBaseUrl: "http://localhost:8080/fhir",
// Optional credentials:
// credentials: { accessToken: "..." }
// credentials: { clientId: "...", clientSecret: "..." }
// credentials: { clientId: "...", privateKey: "..." }
});
await ehr.connect();📚 Unified Domain Services API Reference
All services support strongly-typed search parameters, pagination, and return normalized models with the raw FHIR resource attached under .raw.
| Service | Methods | Description |
| :--- | :--- | :--- |
| ehr.patients | get(id), list(params), search(params) | Demographics, identifiers, contact info, address |
| ehr.observations | get(id), list(params), search(params), iterate(params) | Vital signs, laboratory results, survey answers |
| ehr.encounters | get(id), list(params), search(params), iterate(params) | Inpatient, outpatient, and ambulatory clinical visits |
| ehr.conditions | get(id), list(params), search(params), iterate(params) | Problems, diagnoses, active health concerns |
| ehr.medications | get(id), list(params), search(params), iterate(params) | Active prescriptions, orders, statements, dispense history |
| ehr.allergies | get(id), list(params), search(params), iterate(params) | Drug, food, and environmental allergies & intolerances |
| ehr.procedures | get(id), list(params), search(params), iterate(params) | Surgical operations, interventions, and diagnostics |
| ehr.diagnosticReports | get(id), list(params), search(params), iterate(params) | Radiology reports, pathology summaries, lab panels |
| ehr.documents | get(id), list(params), search(params), iterate(params) | Clinical notes, C-CDA summaries, discharge summaries |
| ehr.appointments | get(id), list(params), search(params), iterate(params) | Scheduled and upcoming patient visits |
| ehr.practitioners | get(id), list(params), search(params) | Clinicians, physicians, nurses, care team members |
Async Streaming Iterators
Avoid downloading thousands of items into memory at once. Use .iterate():
for await (const observation of ehr.observations.iterate({ patientId: "123", category: "laboratory" })) {
console.log(observation.codeText, observation.value, observation.unit);
}🛠 Raw FHIR Client Escape Hatch
When you need direct access to raw FHIR resources, batch operations, or vendor extensions:
// Read raw FHIR Patient
const rawPatient = await ehr.fhir.read("Patient", "123");
// Custom FHIR search
const bundle = await ehr.fhir.search("Observation", {
subject: "Patient/123",
code: "8867-4", // Heart rate
_sort: "-date",
_count: 10
});
// Create new resource
const created = await ehr.fhir.create("Encounter", {
resourceType: "Encounter",
status: "in-progress",
class: { code: "AMB" },
subject: { reference: "Patient/123" }
});
// JSON Patch
await ehr.fhir.patch("Patient", "123", [
{ op: "replace", path: "/gender", value: "female" }
]);
// Server Capabilities
const capabilities = await ehr.fhir.capabilities();🔒 Security, HIPAA & Secret Redaction
EHRConnect is designed from the ground up for strict healthcare security guidelines:
- Zero EHR User Passwords: The library will NEVER prompt for or accept a physician or patient's EHR username/password. All interactions occur via SMART Backend Services (RFC 7523) or OAuth2 PKCE.
- Automatic Secret Redaction: All private keys, client secrets, access tokens, refresh tokens, auth codes, and Bearer authorization headers are automatically masked in loggers, error objects, and serialization (
[REDACTED],Bearer [REDACTED_TOKEN]). - Transient Memory Footprint: In-memory token stores purge sensitive key material on process termination and support encryption decorators.
- Resilience & Rate Limiting: Exponential backoff with full jitter handles HTTP 429 rate limits, HTTP 503 outages, and parses standard
Retry-Afterheaders.
🐳 Local Development with Docker HAPI FHIR
Run a local, unauthenticated open-source HAPI FHIR R4 server with pre-seeded test data in seconds:
# Start HAPI FHIR R4 container
docker compose -f docker/docker-compose.yml up -d
# Seed synthetic patient dataset
pnpm --filter=@ehr-connect/core exec tsx docker/seed-data.ts🧪 Testing & Quality Assurance
EHRConnect has an extensive test suite with 104 tests and 86%+ code coverage:
# Run all unit and integration tests
pnpm test
# Generate coverage report
pnpm test:coverage
# Run TypeScript typechecks
pnpm typecheck
# Lint codebase
pnpm lint📄 License
This library is open-source software licensed under the Apache License 2.0.
