medOS ultra

Acknowledgement System

Universal AcknowledgementRequest (FHIR Task) wrapping any order/note/encounter to request explicit acknowledgement, multi-channel.

5 min read diagramsUpdated 2026-05-01docs/architecture/acknowledgement-system.md

Shipped: 2026-04-29 (commit a77ada04) Migration: infrastructure/medbase/migrations/035_acknowledgement_requests.sql Edge function: infrastructure/medbase/functions/ack-escalator/ FHIR profile: http://medos.health/fhir/StructureDefinition/AcknowledgementTask

Problem

Acknowledgement was fragmented across the codebase:

  • CoSignRequest (foundation) — only references PatientNote; not extensible to lab/imaging/medication/etc.
  • medical-editor wait-acknowledgement workflow state — local-only, no backend persistence.
  • Consultation — only multi-user “ack-like” entity; uses recipientType USER/ROLE/DEPT.
  • No multi-channel delivery (app/web/email/SMS/push), no escalation, no reminders.

The clinic needs any order — lab, imaging, medication, encounter sign-off, discharge approval, billing — to be able to ask any user / role / department / group for an acknowledgement, with a deadline + escalation chain + RRULE-driven reminders + multi-channel delivery.

Shape

Polymorphic AcknowledgementRequest entity that wraps an existing order. Maps cleanly onto FHIR R4 Task with Task.focus pointing at the underlying resource (Task is the FHIR “asked-to-do-something” resource; ServiceRequest is the order).

AcknowledgementRequest
├── subject              { orderType, orderId, display }
├── requester            (User ref)
├── recipient            { recipientType: user|role|department|group, userId|roleId|...}
├── reason, priority     routine|urgent|stat
├── status               PENDING → ACKNOWLEDGED | DECLINED | EXPIRED | ESCALATED | CANCELLED
├── response             { acknowledgedBy, acknowledgedAt, comment, signature }
├── deadlineAt           drives escalation
├── reminderRule         RRULE (rrule lib)
├── nextReminderAt       set by escalator after each reminder
├── escalationChain      [{ afterMinutes, escalateTo: recipient }]
├── escalationStep       cursor into the chain
├── channels             [APP, WEB, EMAIL, SMS, PUSH]
├── deliveryLog          [{ channel, status, attemptedAt, providerMessageId }]
└── encounterRef, patientRef, workflowId, workflowStep

Data flow

Frontend / workflow node
   │
   │ POST /api/foundation/acknowledgementRequests
   ▼
foundation service (NestJS + Moleculer + MongoDB)
   │
   │ broadcasts Moleculer events:
   │   acknowledgement.requested | responded | escalated | reminder
   ▼
messaging service (acknowledgementDispatcher.mixin)
   │
   ├── Socket.IO (APP/WEB) ──→ recipient's room (userId / role:X / dept:Y / group:Z)
   ├── MailTransporterService (EMAIL) ──→ Nodemailer ──→ email
   ├── SMS (feature-flagged, provider TBD)
   ├── Push (feature-flagged, provider TBD)
   └── For each attempt: foundation.acknowledgementRequest.appendDelivery → deliveryLog row
                          ↓
                   hospital_events → encounter-orchestrator (Deno)
                          ↓
              acknowledgement_requests (Supabase read model)
                          ↓
              Supabase realtime ──→ AcknowledgementInbox (FAB + drawer)

Backend modules

Module Path Purpose
Schema packages/platform-api-schema/src/foundation/acknowledgementRequest/ Entity + enums (OrderType, RecipientType, Status, Priority, Channel)
Service services/foundation/.../modules/acknowledgementRequest/ CRUD + respond + escalate + appendDelivery actions
Dispatcher services/messaging/.../modules/acknowledgementDispatcher/ NestJS service + Moleculer event mixin; channel fan-out
Email template services/messaging/views/emails/ack-request.hbs Handlebars body for the EMAIL channel
FHIR Task services/public-api/.../modules/fhir/transformers/ack-to-fhir-task.transformer.ts Bidirectional medOS ↔ FHIR Task mapper
FHIR controller services/public-api/.../modules/fhir/resources/task.controller.ts /fhir/Task (read/search/create/update); merges clinical-task and acknowledgement sources, profile-discriminated for writes

Read model (Supabase)

Object Purpose
acknowledgement_requests Projected from MongoDB via hospital_events → encounter-orchestrator
acknowledgement_inbox (view) Per-user pending list with inbox_key (priority-ordered)
acknowledgement_delivery_log One row per channel attempt (queued/sent/delivered/failed/skipped)
RLS auth.uid() IS NOT NULL AND (recipient_user_id = auth.uid() OR requester_user_id = auth.uid() OR role/dept claim match) via ack_jwt_array_claim() helper
Realtime Published — frontend subscribes via Supabase channels

Escalator (edge function)

infrastructure/medbase/functions/ack-escalator/index.ts — Deno edge function invoked once per minute by a managed pg_cron job (registered in cron_jobs registry — see cron-jobs-registry.md).

State transitions:

PENDING + deadlineAt ≤ now()
   ├── escalation_step < length(escalationChain)
   │     → status='pending' (or 'escalated' if no afterMinutes)
   │     → recipient = chain[step].escalateTo
   │     → deadlineAt = now + chain[step].afterMinutes
   │     → emit hospital_events 'acknowledgement.escalated'
   │
   └── else
         → status='expired'
         → emit hospital_events 'acknowledgement.expired'

PENDING + next_reminder_at ≤ now()
   └── recompute next_reminder_at via reminderRule (RRULE.after(now))
       emit hospital_events 'acknowledgement.reminder' (no status change)

Frontend

Component Path Mount
AcknowledgementInbox web/src/common/components/medical/builder/acknowledgements/AcknowledgementInbox.tsx Drop-in FAB + side drawer; mounted in App.tsx for every authenticated user
AcknowledgementAdminList same dir Ops table (admin-only filterable list)
useAcknowledgementInbox same dir Supabase realtime subscription hook
acknowledgementRequest.service.ts web/src/services/ever-foundation/acknowledgement-request/ Write helpers (create / respond / cancel / listByOrder)
Workflow action web/packages/medical-kit/.../workflow-config/actions.ts ack-create-request payload builder for medical-editor wait-acknowledgement nodes

Channel rollout

Channel v1 default Feature flag
APP / WEB (Socket.IO + Supabase realtime) ✅ on
EMAIL (Nodemailer + ack-request.hbs) ✅ on (when SMTP configured) ACK_CHANNEL_EMAIL_ENABLED
SMS scaffolded, off ACK_CHANNEL_SMS_ENABLED
PUSH scaffolded, off ACK_CHANNEL_PUSH_ENABLED

FHIR mapping

ackToFhirTask(record) produces a Task with:

  • Task.id ← AcknowledgementRequest._id
  • Task.status ← mapped (pending→requested, acknowledged→completed, declined→rejected, expired→failed, escalated→in-progress)
  • Task.intent='order', priority, description
  • Task.focus.reference{ServiceRequest|MedicationRequest|Encounter|...}/{orderId}
  • Task.for ← Patient ref
  • Task.encounter ← Encounter ref
  • Task.owner.referencePractitioner|PractitionerRole|Organization|Group/{recipientId}
  • Task.restriction.period.end ← deadlineAt
  • Task.meta.profile includes the AcknowledgementTask profile URL

fhirTaskToAck(task) is the inverse; the task.controller.ts POST/PUT routes detect the AcknowledgementTask profile and route writes to foundation.acknowledgementRequest.create/update instead of the legacy clinical.task.*.

Verification (smoke tests)

  • 26/26 pure-function transformer tests pass (status mappings, recipient → FHIR resource, focus, profile, round trip)
  • 16/16 SQL structural checks pass on the migration
  • Edge function smoke: POST /functions/v1/ack-escalator returns 200 + {ok:true, summary:{escalated:0, expired:0, reminded:0, errors:0}}
  • REST /rest/v1/{acknowledgement_requests,_inbox,_delivery_log} return [] under anon (RLS enforced)

Deploy state

Surface State Notes
Frontend (Vercel) ✅ auto on git push origin main AcknowledgementInbox is mounted in App.tsx
Supabase migration 035 ✅ applied to hynsmfrevlsegbmjnoiy (PH demo, shared) Manual paste in Studio
Supabase migration 036 (cron_jobs registry) ⚠️ apply via Studio after this PR lands Required to control the escalator schedule
Edge function ack-escalator ✅ deployed Symlinked at infrastructure/medbase/supabase/functions/ack-escalator
pg_cron ack-escalator-tick ⚠️ enable from /super-admin/cron-jobs after setting GUCs See cron-jobs-registry.md
Backend services (foundation/messaging/public-api) ⚠️ manual EC2 deploy required Until then, POST /api/foundation/acknowledgementRequests 404s

Out of scope (v1)

  • Replacing CoSignRequest — leave as-is; can migrate later as subject.orderType='note'.
  • SMS / push provider selection — scaffolded behind feature flags, providers chosen per market.
  • Multi-recipient response aggregation — single-responder MVP.
  • Offline-queue ack on mobile — relies on existing Socket.IO reconnect.
Ask Anything