CQRS & Event-Driven Architecture
Write to MongoDB, read from Postgres, events connect everything, realtime updates reach the UI in milliseconds.
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_eventstable + 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. |