Skip to main content

Platform interoperability — event model (NATS JetStream)

Document version: 1.1
Date: 2026-03-22
Status: Normative companion to SPEC.md

Related documents:


1. Purpose

This document defines the standard event envelope, subject taxonomy, and minimum event contracts for interoperability and licensed add-ons.

It extends core platform eventing to support:

  • FHIR gateway projection + cache invalidation
  • HL7 v2 inbound/outbound workflows
  • Add-on modules: immunizations, care plans, digital communication (messaging + virtual visits; canonical types in specs/modules/digital-communication/EVENT_MODEL.md), patient portal, LIS, PACS, insurance, billing, claims

Note (2026-04): The messaging and standalone virtual-care services are unified into digital-communication (ADR-0047). Legacy ghasi.messaging.* and VIRTUAL_CARE.* subjects may be dual-published during migration.


2. Transport: NATS JetStream

2.1 Streams in the repository (current)

The repo includes JetStream stream configuration in infra/nats/streams.clinical.conf.

Current streams and subject wildcards:

  • REGISTRATIONregistration.>
  • SCHEDULINGscheduling.>
  • PROVIDERSproviders.>
  • FACILITIESfacilities.>
  • CLINICAL_NOTESclinical.notes.>
  • CLINICAL_ORDERSclinical.orders.>
  • CLINICAL_RESULTSclinical.results.>
  • CLINICAL_MEDICATIONSclinical.medications.>
  • CLINICAL_CONDITIONSclinical.conditions.>
  • CLINICAL_ALLERGIESclinical.allergies.>
  • CLINICAL_VITALSclinical.vitals.>
  • CHARTchart.>

2.2 Stream additions (required)

Add the following streams (or expand existing wildcard coverage) to support add-on modules:

  • CLINICAL_IMMUNIZATIONSclinical.immunizations.>
  • CLINICAL_CAREPLANSclinical.careplans.>
  • ENGAGE_MESSAGINGengage.messaging.>
  • PORTALportal.> (optional; can be derived from other domains)
  • DIAG_LABdiag.laboratory.>
  • DIAG_RADIOLOGYdiag.radiology.>
  • FIN_BILLINGbilling.>
  • FIN_INSURANCEinsurance.>
  • FIN_CLAIMSclaims.>
  • INTEROPinterop.>
  • FHIRfhir.> (optional; see projections section)

3. Event Envelope

3.1 Standard envelope (CloudEvents-style)

Use CloudEventsBuilder from @ghasi/nats-client for all new interop and add-on events.

Envelope fields (per builder):

  • id (UUID)
  • time (ISO string)
  • source (e.g., ghasi/registration)
  • type (e.g., ghasi.registration.patient.created)
  • tenantid
  • actorid
  • actortype (USER | SERVICE)
  • correlationid
  • nodeid (optional)
  • dataversion (default 1)
  • data (payload)

3.2 Backward compatibility note

Some existing services publish non-CloudEvents payloads (e.g., clinical.allergies.events, clinical.vitals.{patientId}) with an eventType string in the body.

Requirement:

  • Do not introduce new non-CloudEvents formats.
  • For existing legacy formats, support consumption as-is, but converge over time to CloudEvents envelope.

4. Subject Taxonomy

4.1 Subject naming standard

Use lowercase, dot-delimited subjects:

{domain}.{entity}.{action}

Examples:

  • registration.patient.created
  • clinical.orders.placed
  • diag.laboratory.result.released
  • interop.hl7v2.adt.received

4.2 Current repo inconsistencies

The stream configuration uses lowercase subject wildcards (e.g., registration.>), while some publishers currently use uppercase prefixes (e.g., REGISTRATION.patient.created).

Requirement:

  • All new subjects MUST follow the lowercase standard.
  • A follow-up refactor should align legacy publishers to lowercase to match JetStream stream bindings.

5. Minimum event catalog

This catalog lists required event types and recommended subjects.

5.1 Registration (existing; CloudEvents)

  • Subject: registration.patient.created

    • Type: ghasi.registration.patient.created
    • Data: { patientId, mrn, tenantId }
  • Subject: registration.patient.updated

    • Type: ghasi.registration.patient.updated
    • Data: { patientId, mrn, tenantId }
  • Subject: registration.patient.merged

    • Type: ghasi.registration.patient.merged
    • Data: { survivorId, sourceId, tenantId }
  • Subject: registration.encounter.registered

    • Type: ghasi.registration.encounter.registered
    • Data: { encounterId, patientId, encounterType, status, tenantId }

5.2 Orders / Results → Interop triggers

  • Subject: clinical.orders.placed

    • Type: ghasi.orders.placed
    • Use: triggers HL7 v2 ORM/OML outbound and LIS/RIS routing
  • Subject: clinical.results.released

    • Type: ghasi.results.released
    • Use: triggers HL7 v2 ORU outbound; portal notification eligibility

5.3 Immunizations

  • Subject: clinical.immunizations.recorded

    • Type: ghasi.immunizations.recorded
    • Data: { immunizationId, patientId, encounterId?, lotNumber?, cvxCode, tenantId }
  • Subject: clinical.immunizations.refused

    • Type: ghasi.immunizations.refused
    • Data: { refusalId, patientId, reasonCode?, tenantId }

5.4 Care Plans

  • Subject: clinical.careplans.created

    • Type: ghasi.careplans.created
    • Data: { carePlanId, patientId, status, tenantId }
  • Subject: clinical.careplans.reviewed

    • Type: ghasi.careplans.reviewed
    • Data: { carePlanId, patientId, reviewerId, tenantId }

5.5 Messaging

JetStream stream: engage.messaging.events (see apps/services/digital-communication publisher).

Canonical CloudEvents type values (consumers should match on type):

Typedata payload (minimum)
ghasi.messaging.sent{ threadId, messageId, senderId, tenantId, urgency, patientId? }
ghasi.messaging.repliedSame as sent
ghasi.messaging.read{ threadId, messageIds[], userId, tenantId }
ghasi.messaging.escalated{ threadId, escalatedBy, tenantId, reason? }

Recommended NATS subject (lowercase, for wildcard routing): engage.messaging.sent, engage.messaging.read, etc., when subjects are used in addition to stream binding.

Legacy (deprecated): earlier drafts used ghasi.messaging.message.sent / ghasi.messaging.message.read with recipientIds in payloads. New subscribers MUST use the canonical types in the table above; recipientIds is not required by the current messaging service implementation (participants are derived server-side).

Normative module doc: specs/modules/messaging/EVENT_MODEL.md.

5.6 Portal

Portal should primarily consume domain events and generate notifications.

  • Subject: portal.notification.queued
    • Type: ghasi.portal.notification.queued
    • Data: { notificationId, patientId, channel, template, tenantId }

5.7 LIS / PACS

  • Subject: diag.laboratory.accession.created

    • Type: ghasi.lis.accession.created
    • Data: { accessionId, patientId, orderId, tenantId }
  • Subject: diag.laboratory.result.verified

    • Type: ghasi.lis.result.verified
    • Data: { resultId, patientId, tenantId }
  • Subject: diag.laboratory.result.released

    • Type: ghasi.lis.result.released
    • Data: { resultId, patientId, tenantId }
  • Subject: diag.radiology.study.available

    • Type: ghasi.radiology.study.available
    • Data: { studyId, patientId, accessionId?, tenantId }
  • Subject: diag.radiology.report.signed

    • Type: ghasi.radiology.report.signed
    • Data: { reportId, studyId, patientId, tenantId }

5.8 Insurance / Billing / Claims

  • Subject: insurance.coverage.created

    • Type: ghasi.insurance.coverage.created
    • Data: { coverageId, patientId, payerId, tenantId }
  • Subject: billing.charge.posted

    • Type: ghasi.billing.charge.posted
    • Data: { chargeId, patientId, encounterId, tenantId }
  • Subject: claims.submitted

    • Type: ghasi.claims.submitted
    • Data: { claimId, patientId, payerId, tenantId }
  • Subject: claims.remittance.received

    • Type: ghasi.claims.remittance.received
    • Data: { remittanceId, payerId, tenantId }

5.9 HL7 v2 interop

  • Subject: interop.hl7v2.adt.received

    • Type: ghasi.interop.hl7v2.adt.received
    • Data: { messageId, connectorId, messageType, tenantId }
  • Subject: interop.hl7v2.orm.sent

    • Type: ghasi.interop.hl7v2.orm.sent
    • Data: { messageId, connectorId, orderId, tenantId }
  • Subject: interop.hl7v2.oru.sent

    • Type: ghasi.interop.hl7v2.oru.sent
    • Data: { messageId, connectorId, resultId, tenantId }

6. Consumer Requirements

6.1 Idempotency

All consumers MUST be idempotent.

Minimum strategy:

  • Maintain a processed_events table keyed by (tenant_id, event_id) OR
  • Use JetStream message metadata + a durable consumer with explicit acks and a dedupe store.

6.2 Retry and dead-letter

Define per-consumer:

  • maxDeliver
  • exponential backoff
  • DLQ subject pattern: dlq.{originalSubject}

Persist failed messages with:

  • event_id, subject, type, tenantid, correlationid, error stack, first/last attempt timestamps.