medOS ultra

CQRS & Event-Driven Architecture

Write to MongoDB, read from Postgres, events connect everything, realtime updates reach the UI in milliseconds.

5 min read diagramsUpdated 2026-03-26docs/architecture/architecture/06-cqrs-event-driven-architecture.md

Command / query split

flowchart LR
  CMD["Commands<br/>(writes)"] --> SVC["Moleculer services"]
  SVC --> MONGO[("MongoDB<br/>write model")]
  MONGO -->|domain events| ORCH["encounter-orchestrator"]
  ORCH --> PG[("Postgres<br/>read model")]
  PG --> Q["Queries<br/>(realtime reads)"]
  Q -.-> UI["Worklists · dashboards · Patient 360"]

Write to MongoDB. Read from Postgres. Events connect everything. Realtime updates reach the UI in milliseconds.


Overview

MedOS implements CQRS (Command Query Responsibility Segregation) with an event-driven backbone:

  • Write Model: MongoDB via Moleculer microservices — source of truth for all clinical, financial, and administrative data
  • Read Model: Supabase Postgres — query-optimized caches for worklists, dashboards, and AI context
  • Event Bridge: hospital_events table + Deno Edge Functions — guaranteed event delivery
  • Realtime Layer: Supabase Realtime (WebSocket) — sub-second UI updates

Critical Rule: Frontend reads from Postgres, writes to Moleculer APIs. Never the reverse.


Architecture Diagram

┌──────────────────────────────────────────────────────────────────────────────┐
│                         COMMAND SIDE (Write)                                  │
│                                                                              │
│  React UI → API Request → ever-api-gateway → Domain Microservice             │
│                                                   │                          │
│                              ┌────────────────────┤                          │
│                              │                    │                          │
│                              ▼                    ▼                          │
│                     MongoDB Write         ctx.emit('clinical.*')             │
│                     (Source of Truth)      (Moleculer Event Bus)              │
└──────────────────────────────────────────────────┬───────────────────────────┘
                                                   │
                              ┌─────────────────────┤
                              │                     │
                              ▼                     ▼
              ┌───────────────────────┐  ┌──────────────────────────┐
              │  GUARANTEED PATH      │  │  OPTIONAL PATH           │
              │  (Always fires)       │  │  (Only if subscribed)    │
              │                       │  │                          │
              │  InternalEventBridge   │  │  WebhookDispatcher       │
              │  → INSERT INTO        │  │  → Query subscriptions   │
              │    hospital_events    │  │  → HMAC-sign payload     │
              │  → PG Trigger fires   │  │  → POST to external URL  │
              │  → pg_net.http_post() │  │  → Log delivery result   │
              │  → Deno Edge Function │  │                          │
              └───────────┬───────────┘  └──────────────────────────┘
                          │
                          ▼
┌──────────────────────────────────────────────────────────────────────────────┐
│                     QUERY SIDE (Read Model Projection)                        │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────────┐ │
│  │  encounter-orchestrator (Deno Edge Function, ~30k LOC)                  │ │
│  │                                                                         │ │
│  │  1. Normalize event (legacy → manifest.* schema)                        │ │
│  │  2. Dedup by last_event_id                                              │ │
│  │  3. Update encounter_journey_cache:                                     │ │
│  │     • pending_tickets (med, lab, imaging, procedure...)                 │ │
│  │     • completed_tickets                                                 │ │
│  │     • clinical_context (patient, alerts, coding, RCM)                   │ │
│  │     • financial_summary, claim_summary                                  │ │
│  │  4. Route to department_queues (by order category)                      │ │
│  │  5. Prune active_alerts (max 50)                                        │ │
│  │  6. On error → Dead Letter Queue                                        │ │
│  └─────────────────────────────────────────────────────────────────────────┘ │
│                                                                              │
│  ┌──────────────────────────┐  ┌──────────────────────────────────────────┐ │
│  │ encounter_journey_cache  │  │ department_queues                        │ │
│  │ ────────────────────────  │  │ ──────────────────                      │ │
│  │ Patient-centric manifest │  │ Per-department worklist                  │ │
│  │ encounter_id (PK)        │  │ dept_type + location_id                  │ │
│  │ pending_tickets (JSONB)  │  │ ticket_id, status, priority              │ │
│  │ completed_tickets (JSONB)│  │ patient_hn, patient_name (denormalized)  │ │
│  │ clinical_context (JSONB) │  │ metadata (JSONB)                         │ │
│  │ financial_summary (JSONB)│  │                                          │ │
│  │ active_alerts (JSONB[])  │  │ Realtime: filter by location_id          │ │
│  │ has_active_allergies     │  │ (each nurse screen → own department)     │ │
│  │ has_active_medications   │  │                                          │ │
│  └──────────────────────────┘  └──────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌──────────────────────────────────────────────────────────────────────────────┐
│                     REALTIME DELIVERY (WebSocket)                             │
│                                                                              │
│  Supabase Realtime (postgres_changes)                                        │
│                                                                              │
│  ┌─────────────────────────────────┐  ┌──────────────────────────────────┐  │
│  │  Single Encounter Subscription  │  │  Department Queue Subscription   │  │
│  │  channel: manifest-row-{id}     │  │  channel: dept-queue-{type}      │  │
│  │  table: encounter_journey_cache │  │  table: department_queues        │  │
│  │  filter: encounter_id=eq.{id}   │  │  filter: location_id=eq.{loc}   │  │
│  │  event: UPDATE                  │  │  event: * (INSERT/UPDATE/DELETE) │  │
│  └─────────────────────────────────┘  └──────────────────────────────────┘  │
│                                                                              │
│  Frontend: manifest-data-layer service                                       │
│  ├── SupabaseManifestTransport (realtime + fetch)                           │
│  ├── SubscriptionBroker (dedup: refCount per encounter)                     │
│  └── Hooks: useManifestRow, useManifestList, useManifestCounts              │
└──────────────────────────────────────────────────────────────────────────────┘

Event Contract

Legacy Events (current producers)

ORDER_CREATED, TICKET_UPDATED, TICKET_COMPLETED
ENCOUNTER_CLOSED, ENCOUNTER_STATUS_UPDATED
CLINICAL_UPDATE, CLINICAL_ALERT
CODING_STARTED, CODING_COMPLETED, CODING_AI_SUGGESTION_GENERATED
CLAIM_ACCOUNT_CLOSED, CLAIM_INVOICE_GENERATED, CLAIM_SETTLED

Normalized Events (manifest.* prefix)

manifest.encounter.seeded / .progressed / .closed
manifest.order.created / .updated / .completed
manifest.clinical.alert
manifest.coding.started / .completed / .ai_suggestion
manifest.claim.settled / .invoice_generated

PRM Events (own prefix)

prm.opportunity.created / .approved / .sent
prm.consent.granted / .revoked
prm.trial.enrolled

Normalization: normalizeEventType() maps legacy → manifest.* at the orchestrator boundary.


Webhook System (Side-App Integration)

The WebhookDispatcher enables external systems to subscribe to any event:

┌──────────────┐     HMAC-SHA256        ┌────────────────────────┐
│  Moleculer    │ ───────────────────▶  │  External System       │
│  Event Bus    │   X-Vajira-Event      │  (n8n, your app, etc.) │
│               │   X-Vajira-Signature  │                        │
│  clinical.*   │   X-Vajira-Delivery-ID│  Verify signature      │
│  financial.*  │                       │  Process event          │
│  medication.* │   Timeout: 10s        │  Build your features    │
│  prm.*        │   Audit: delivery log │                        │
└──────────────┘                        └────────────────────────┘

Key design: Webhooks are optional. Delete all subscriptions — the hospital still works. The guaranteed path (hospital_events → trigger → Deno) always fires independently.


Resilience Patterns

Pattern Implementation
Dead Letter Queue Failed events → medbase_dead_letter_queue with error context for replay
Event Deduplication last_event_id on encounter_journey_cache prevents re-processing
Alert Pruning Max 50 active alerts per encounter (configurable via medbase_config)
Event Retention 90-day default retention on hospital_events
Cache TTL 30-day TTL for discharged encounter caches
Denormalization patient_hn, patient_name on department_queues avoids MongoDB round-trip

Frontend Subscription Architecture

// Single encounter — doctor viewing patient
const { row } = useManifestRow(encounterId)
// Subscribes to encounter_journey_cache where encounter_id = X
// Auto-updates on any backend event

// Department queue — nurse station
const { items } = useManifestList({ deptType: 'consultation', locationId })
// Subscribes to department_queues filtered by location
// Each nurse screen only sees their department

// Derived values — dashboard KPI
const { counts } = useManifestCounts({ locationId })
// Computes pending/completed/total from cached rows

SubscriptionBroker prevents duplicate WebSocket subscriptions when multiple React components mount for the same encounter. Ref-counting ensures one underlying channel per encounter_id.


Why This Matters

For the CTO Benefit
Decoupled teams Backend writes to MongoDB. Frontend reads from Postgres. Neither blocks the other.
Sub-second UI Supabase Realtime delivers updates in <100ms after backend write
Side-app ecosystem HMAC webhooks let partners build on your event stream
AI readiness clinical_context JSONB + has_active_allergies flags enable AI reasoning without MongoDB access
Audit trail Every event persisted in hospital_events. DLQ catches failures.
Location-aware Nurse at Station A only sees Station A’s queue. Zero cross-contamination.
Ask Anything