Platform interoperability — event model (NATS JetStream)
Document version: 1.1
Date: 2026-03-22
Status: Normative companion to SPEC.md
Related documents:
- Specification: SPEC.md
- Technical Requirements: TECHNICAL_REQUIREMENTS.md
- API Documentation: API_DOCS.md
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:
REGISTRATION→registration.>SCHEDULING→scheduling.>PROVIDERS→providers.>FACILITIES→facilities.>CLINICAL_NOTES→clinical.notes.>CLINICAL_ORDERS→clinical.orders.>CLINICAL_RESULTS→clinical.results.>CLINICAL_MEDICATIONS→clinical.medications.>CLINICAL_CONDITIONS→clinical.conditions.>CLINICAL_ALLERGIES→clinical.allergies.>CLINICAL_VITALS→clinical.vitals.>CHART→chart.>
2.2 Stream additions (required)
Add the following streams (or expand existing wildcard coverage) to support add-on modules:
CLINICAL_IMMUNIZATIONS→clinical.immunizations.>CLINICAL_CAREPLANS→clinical.careplans.>ENGAGE_MESSAGING→engage.messaging.>PORTAL→portal.>(optional; can be derived from other domains)DIAG_LAB→diag.laboratory.>DIAG_RADIOLOGY→diag.radiology.>FIN_BILLING→billing.>FIN_INSURANCE→insurance.>FIN_CLAIMS→claims.>INTEROP→interop.>FHIR→fhir.>(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)tenantidactoridactortype(USER|SERVICE)correlationidnodeid(optional)dataversion(default1)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.createdclinical.orders.placeddiag.laboratory.result.releasedinterop.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 }
- Type:
-
Subject:
registration.patient.updated- Type:
ghasi.registration.patient.updated - Data:
{ patientId, mrn, tenantId }
- Type:
-
Subject:
registration.patient.merged- Type:
ghasi.registration.patient.merged - Data:
{ survivorId, sourceId, tenantId }
- Type:
-
Subject:
registration.encounter.registered- Type:
ghasi.registration.encounter.registered - Data:
{ encounterId, patientId, encounterType, status, tenantId }
- Type:
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
- Type:
-
Subject:
clinical.results.released- Type:
ghasi.results.released - Use: triggers HL7 v2 ORU outbound; portal notification eligibility
- Type:
5.3 Immunizations
-
Subject:
clinical.immunizations.recorded- Type:
ghasi.immunizations.recorded - Data:
{ immunizationId, patientId, encounterId?, lotNumber?, cvxCode, tenantId }
- Type:
-
Subject:
clinical.immunizations.refused- Type:
ghasi.immunizations.refused - Data:
{ refusalId, patientId, reasonCode?, tenantId }
- Type:
5.4 Care Plans
-
Subject:
clinical.careplans.created- Type:
ghasi.careplans.created - Data:
{ carePlanId, patientId, status, tenantId }
- Type:
-
Subject:
clinical.careplans.reviewed- Type:
ghasi.careplans.reviewed - Data:
{ carePlanId, patientId, reviewerId, tenantId }
- Type:
5.5 Messaging
JetStream stream: engage.messaging.events (see apps/services/digital-communication publisher).
Canonical CloudEvents type values (consumers should match on type):
| Type | data payload (minimum) |
|---|---|
ghasi.messaging.sent | { threadId, messageId, senderId, tenantId, urgency, patientId? } |
ghasi.messaging.replied | Same 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 }
- Type:
5.7 LIS / PACS
-
Subject:
diag.laboratory.accession.created- Type:
ghasi.lis.accession.created - Data:
{ accessionId, patientId, orderId, tenantId }
- Type:
-
Subject:
diag.laboratory.result.verified- Type:
ghasi.lis.result.verified - Data:
{ resultId, patientId, tenantId }
- Type:
-
Subject:
diag.laboratory.result.released- Type:
ghasi.lis.result.released - Data:
{ resultId, patientId, tenantId }
- Type:
-
Subject:
diag.radiology.study.available- Type:
ghasi.radiology.study.available - Data:
{ studyId, patientId, accessionId?, tenantId }
- Type:
-
Subject:
diag.radiology.report.signed- Type:
ghasi.radiology.report.signed - Data:
{ reportId, studyId, patientId, tenantId }
- Type:
5.8 Insurance / Billing / Claims
-
Subject:
insurance.coverage.created- Type:
ghasi.insurance.coverage.created - Data:
{ coverageId, patientId, payerId, tenantId }
- Type:
-
Subject:
billing.charge.posted- Type:
ghasi.billing.charge.posted - Data:
{ chargeId, patientId, encounterId, tenantId }
- Type:
-
Subject:
claims.submitted- Type:
ghasi.claims.submitted - Data:
{ claimId, patientId, payerId, tenantId }
- Type:
-
Subject:
claims.remittance.received- Type:
ghasi.claims.remittance.received - Data:
{ remittanceId, payerId, tenantId }
- Type:
5.9 HL7 v2 interop
-
Subject:
interop.hl7v2.adt.received- Type:
ghasi.interop.hl7v2.adt.received - Data:
{ messageId, connectorId, messageType, tenantId }
- Type:
-
Subject:
interop.hl7v2.orm.sent- Type:
ghasi.interop.hl7v2.orm.sent - Data:
{ messageId, connectorId, orderId, tenantId }
- Type:
-
Subject:
interop.hl7v2.oru.sent- Type:
ghasi.interop.hl7v2.oru.sent - Data:
{ messageId, connectorId, resultId, tenantId }
- Type:
6. Consumer Requirements
6.1 Idempotency
All consumers MUST be idempotent.
Minimum strategy:
- Maintain a
processed_eventstable 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.