FHIR Source

Expose a FHIR-compliant REST endpoint or subscribe to FHIR Subscriptions for real-time clinical data exchange.

Overview

The FHIR source creates a FHIR-compliant HTTP server that receives FHIR resources via standard REST interactions (create, update) or through FHIR Subscription notifications. It supports both FHIR R4 and STU3 versions.

FHIR (Fast Healthcare Interoperability Resources) is the modern standard for healthcare data exchange. This source enables intu to receive data from EHR systems, health information exchanges, patient portals, and other FHIR-enabled applications using the native FHIR protocol.

Note When using subscription_type, intu registers as a FHIR Subscription endpoint and receives push notifications whenever matching resources are created or updated on the remote FHIR server.

Configuration

yaml
listener:
  type: fhir
  fhir:
    port: 8090
    base_path: /fhir
    version: R4
    subscription_type: rest-hook
    tls:
      cert_file: /etc/intu/certs/fhir.crt
      key_file: /etc/intu/certs/fhir.key
    auth:
      type: oauth2
      issuer: https://auth.hospital.example.com
      audience: intu-fhir-endpoint
      jwks_url: https://auth.hospital.example.com/.well-known/jwks.json

Properties

port int required
The TCP port the FHIR server listens on.
base_path string optional
Base URL path for the FHIR endpoint. Defaults to /fhir. Resource-specific routes are appended automatically (e.g. /fhir/Patient).
version string optional
FHIR specification version. Supported values: R4 (default), STU3.
subscription_type string optional
FHIR Subscription channel type. Supported values: rest-hook (HTTP callbacks), websocket. When omitted, the source operates as a standard FHIR REST endpoint.
tls object optional
TLS configuration for HTTPS. Contains cert_file and key_file.
auth object optional
Authentication configuration. Supports bearer, oauth2, and smart (SMART on FHIR) types.

Subscription Types

Type Description Use Case
rest-hook Receives HTTP POST notifications when subscribed resources change. Real-time event-driven workflows where the FHIR server supports Subscriptions.
websocket Maintains a persistent WebSocket connection for streaming notifications. Low-latency scenarios requiring immediate notification delivery.

Complete Example

A FHIR R4 endpoint that receives Patient resources via REST interactions and forwards them to an internal system after validation and transformation.

yaml
id: fhir-patient-ingest
enabled: true

listener:
  type: fhir
  fhir:
    port: 8090
    base_path: /fhir
    version: R4
    tls:
      cert_file: /etc/intu/certs/fhir.crt
      key_file: /etc/intu/certs/fhir.key
    auth:
      type: oauth2
      issuer: https://auth.hospital.example.com
      audience: intu-fhir-endpoint
      jwks_url: https://auth.hospital.example.com/.well-known/jwks.json

validator:
  runtime: node
  entrypoint: validator.ts

transformer:
  runtime: node
  entrypoint: transformer.ts

destinations:
  - master-patient-index
  - audit-trail

TypeScript Transformer Example

This transformer validates an incoming FHIR Bundle, extracts Patient and related resources, and produces a normalized output for the master patient index.

typescript
import { Message, TransformResult } from "@intu/sdk";

interface FHIRResource {
  resourceType: string;
  id?: string;
  [key: string]: unknown;
}

interface FHIRBundle {
  resourceType: "Bundle";
  type: string;
  entry?: { resource: FHIRResource }[];
}

interface NormalizedPatient {
  mrn: string;
  source: string;
  demographics: {
    familyName: string;
    givenNames: string[];
    birthDate: string;
    gender: string;
    deceasedBoolean: boolean;
  };
  identifiers: { system: string; value: string }[];
  contacts: { system: string; value: string; use: string }[];
  addresses: {
    use: string;
    lines: string[];
    city: string;
    state: string;
    postalCode: string;
    country: string;
  }[];
}

export default function transform(msg: Message): TransformResult {
  let resource: FHIRResource | FHIRBundle;
  try {
    resource = JSON.parse(msg.body as string);
  } catch {
    return { success: false, error: "Invalid JSON in FHIR payload" };
  }

  const patients: FHIRResource[] = [];

  if (resource.resourceType === "Bundle") {
    const bundle = resource as FHIRBundle;
    for (const entry of bundle.entry || []) {
      if (entry.resource?.resourceType === "Patient") {
        patients.push(entry.resource);
      }
    }
  } else if (resource.resourceType === "Patient") {
    patients.push(resource);
  } else {
    return { success: false, error: `Unsupported resource type: ${resource.resourceType}` };
  }

  if (patients.length === 0) {
    return { success: false, error: "No Patient resources found" };
  }

  const normalized: NormalizedPatient[] = patients.map((pt) => {
    const names = (pt.name as any[]) || [];
    const official = names.find((n: any) => n.use === "official") || names[0] || {};
    const identifiers = (pt.identifier as any[]) || [];
    const telecoms = (pt.telecom as any[]) || [];
    const addrs = (pt.address as any[]) || [];

    const mrn = identifiers.find(
      (id: any) =>
        id.type?.coding?.some((c: any) => c.code === "MR")
    );

    return {
      mrn: mrn?.value || pt.id || "",
      source: msg.metadata?.source || "fhir-ingest",
      demographics: {
        familyName: official.family || "",
        givenNames: official.given || [],
        birthDate: (pt.birthDate as string) || "",
        gender: (pt.gender as string) || "unknown",
        deceasedBoolean: (pt.deceasedBoolean as boolean) || false,
      },
      identifiers: identifiers.map((id: any) => ({
        system: id.system || "",
        value: id.value || "",
      })),
      contacts: telecoms.map((t: any) => ({
        system: t.system || "",
        value: t.value || "",
        use: t.use || "",
      })),
      addresses: addrs.map((a: any) => ({
        use: a.use || "",
        lines: a.line || [],
        city: a.city || "",
        state: a.state || "",
        postalCode: a.postalCode || "",
        country: a.country || "",
      })),
    };
  });

  return {
    success: true,
    body: JSON.stringify(normalized.length === 1 ? normalized[0] : normalized),
    contentType: "application/json",
  };
}