@fhirfly-io/shl
v0.6.0
Published
Official FHIRfly SDK for SMART Health Links - IPS bundle creation and SHL sharing
Maintainers
Readme
@fhirfly-io/shl
SMART Health Links SDK for Node.js — build IPS FHIR Bundles from clinical codes, encrypt, and share via SHL/QR code.
What It Does
Takes raw clinical data (NDC codes, ICD-10 codes, RxNorm, LOINC, CVX) and produces a shareable SMART Health Link:
Raw codes + patient info → Enriched FHIR Bundle → Encrypted SHL → QR codePHI never leaves your server. Only terminology codes are sent to FHIRfly for enrichment — no BAA required.
Prerequisites
- Node.js 18 or later
- FHIRfly API key — optional, for code enrichment (display names, SNOMED mappings). Get a free key. Without it, use manual
code/system/displayinput orbySNOMED/fromResource(no API call needed).
Installation
npm install @fhirfly-io/shlFor FHIRfly API enrichment (optional — adds display names and SNOMED cross-mappings):
npm install @fhirfly-io/shl @fhirfly-io/terminologyQuick Start
Without FHIRfly API (no key needed)
import { IPS, SHL } from "@fhirfly-io/shl";
const bundle = new IPS.Bundle({
name: "Maria Garcia",
birthDate: "1985-03-15",
gender: "female",
});
// Manual coding — no API dependency
bundle.addMedication({ code: "376988009", system: "http://snomed.info/sct", display: "Levothyroxine" });
bundle.addCondition({ code: "44054006", system: "http://snomed.info/sct", display: "Type 2 diabetes" });
bundle.addAllergy({ bySNOMED: "387207008" });
const fhirBundle = await bundle.build();
const storage = new SHL.LocalStorage({ directory: "./shl-data", baseUrl: "http://localhost:3456/shl" });
const result = await SHL.create({ bundle: fhirBundle, storage, passcode: "1234" });
console.log(result.url); // shlink:/eyJ...
console.log(result.qrCode); // data:image/png;base64,...With FHIRfly API (enriched)
import { IPS, SHL } from "@fhirfly-io/shl";
import Fhirfly from "@fhirfly-io/terminology";
const client = new Fhirfly({ apiKey: process.env.FHIRFLY_API_KEY });
const bundle = new IPS.Bundle({
name: "Maria Garcia",
birthDate: "1985-03-15",
gender: "female",
});
// Code-based input — FHIRfly enriches with display names, SNOMED mappings, etc.
bundle.addMedication({ byNDC: "00071015523", fhirfly: client.ndc });
bundle.addCondition({ byICD10: "E11.9", fhirfly: client.icd10 });
bundle.addAllergy({ bySNOMED: "387207008" });
bundle.addImmunization({ byCVX: "208", fhirfly: client.cvx });
bundle.addResult({ byLOINC: "2339-0", fhirfly: client.loinc, value: 95, unit: "mg/dL" });
bundle.addDocument({ content: pdfBuffer, contentType: "application/pdf", title: "Visit Summary" });
const fhirBundle = await bundle.build();
// FhirflyStorage — zero infrastructure, included free in all plans
const storage = new SHL.FhirflyStorage({ apiKey: process.env.FHIRFLY_API_KEY });
const result = await SHL.create({
bundle: fhirBundle,
storage,
passcode: "1234",
label: "Maria's Health Summary",
expiresAt: "travel", // 90 days — or use "point-of-care" (15min), "appointment" (24h), "permanent"
});
console.log(result.url); // shlink:/eyJ1cmwiOiJodHRwczovL...
console.log(result.qrCode); // data:image/png;base64,...
console.log(result.passcode); // "1234"Expiration Presets
Instead of calculating Date objects, use named presets:
await SHL.create({ bundle, storage, expiresAt: "point-of-care" }); // 15 minutes
await SHL.create({ bundle, storage, expiresAt: "appointment" }); // 24 hours
await SHL.create({ bundle, storage, expiresAt: "travel" }); // 90 days
await SHL.create({ bundle, storage, expiresAt: "permanent" }); // no expiration
await SHL.create({ bundle, storage, expiresAt: new Date(...) }); // raw Date still worksPSHD Compliance
For CMS-aligned patient-to-provider sharing at the point of care:
const fhirBundle = await bundle.build({ profile: "pshd" }); // strips meta.profile, uses collection bundle
const result = await SHL.create({
bundle: fhirBundle,
storage,
compliance: "pshd",
expiresAt: "point-of-care",
});See the PSHD Guide for full details.
Storage Adapters
// FHIRfly hosted (zero infrastructure, recommended)
new SHL.FhirflyStorage({ apiKey: "..." });
// AWS S3
new SHL.S3Storage({ bucket: "my-bucket", region: "us-east-1", baseUrl: "https://shl.example.com" });
// Azure Blob Storage
new SHL.AzureStorage({ container: "shl-data", connectionString: "...", baseUrl: "https://shl.example.com" });
// Google Cloud Storage
new SHL.GCSStorage({ bucket: "my-bucket", baseUrl: "https://shl.example.com" });
// Local filesystem (development)
new SHL.LocalStorage({ directory: "./shl-data", baseUrl: "http://localhost:3456/shl" });Input Formats
Each add* method supports multiple input formats:
// From codes (enriched via FHIRfly API)
bundle.addMedication({ byNDC: "00071015523", fhirfly: client.ndc });
bundle.addMedication({ byRxNorm: "161", fhirfly: client.rxnorm });
bundle.addCondition({ byICD10: "E11.9", fhirfly: client.icd10 });
bundle.addResult({ byLOINC: "2339-0", fhirfly: client.loinc, value: 95, unit: "mg/dL" });
bundle.addImmunization({ byCVX: "208", fhirfly: client.cvx });
// From SNOMED (no API call needed)
bundle.addMedication({ bySNOMED: "376988009" });
bundle.addAllergy({ bySNOMED: "387207008" });
// From existing FHIR R4 resources
bundle.addMedication({ fromResource: existingMedicationStatement });
bundle.addCondition({ fromResource: existingCondition });
// Manual coding (no API dependency)
bundle.addMedication({ code: "376988009", system: "http://snomed.info/sct", display: "Levothyroxine" });CLI
npx @fhirfly-io/shl validate bundle.json # Validate a FHIR Bundle
npx @fhirfly-io/shl create bundle.json # Create an SHL from a bundle
npx @fhirfly-io/shl decode shlink:/eyJ... # Decode an SHL URL
npx @fhirfly-io/shl serve # Start a local SHL server
npx @fhirfly-io/shl demo # Full round-trip demoServer Middleware
Host your own SHL endpoints:
import express from "express";
import { expressMiddleware } from "@fhirfly-io/shl/express";
import { ServerLocalStorage } from "@fhirfly-io/shl/server";
const storage = new ServerLocalStorage({
directory: "./shl-data",
baseUrl: "http://localhost:3000/shl",
});
const app = express();
app.use("/shl", expressMiddleware({ storage }));
app.listen(3000);Also available for Fastify (@fhirfly-io/shl/fastify) and Lambda (@fhirfly-io/shl/lambda).
Audit Logging
Use AuditableStorage to capture access events at the storage level — plug in any logging backend:
import { AuditableStorage, SHLServerStorage, AccessEvent } from "@fhirfly-io/shl/server";
class MyAuditStorage extends ServerLocalStorage implements AuditableStorage {
async onAccess(shlId: string, event: AccessEvent): Promise<void> {
await db.auditLog.insert({
shlId,
recipient: event.recipient,
ip: event.ip,
timestamp: event.timestamp,
});
}
}
app.use("/shl", expressMiddleware({ storage: new MyAuditStorage({ ... }) }));The server handler detects AuditableStorage at runtime via isAuditableStorage() — existing storage implementations work unchanged.
Live Exercise
Run the comprehensive integration test against the live API to exercise every SDK path:
npx tsx examples/live-exercise/index.ts --api-key <your-key> --verboseCovers bundle building, FhirflyStorage, LocalStorage + Express, SHL consumption, access control, and edge cases. See examples/live-exercise/README.md for details.
Related
- EHR Integration Guide — Map HL7v2, CCDA, FHIR R4, and flat database records to IPS bundles
- Security & Compliance — Zero-knowledge architecture, HIPAA, compliance checklist
- @fhirfly-io/terminology — FHIRfly terminology API SDK
- SMART Health Links Spec
- IPS Implementation Guide
- FHIRfly SHL Docs
- SHL Viewer
License
MIT
