09-communicationswave: W5filled7 citations

Messenger — In-App Clinical Messaging

Audiences: doctor, clinical-buyer, developer

Messenger — In-App Clinical Messaging

Persistent, multi-party in-platform messaging that keeps all clinical conversations tenant-scoped, SSE-delivered, and fully auditable — without exposing patient data over third-party channels.

Business Purpose

Clinics need a compliant communication layer that stays inside the platform. External channels (SMS, WhatsApp, Telegram) carry risks around data residency and GDPR-regulated health data. The Messenger module provides a WhatsApp-style UX — direct messages, group channels, broadcast announcements, emoji reactions, read receipts, and file attachments — entirely within the clinic's own tenant boundary. For providers that also need external channel reach, bridge adapters relay outbound messages to Telegram and WhatsApp while keeping the message record authoritative inside the platform.

Commercial value: reduces out-of-band communication (personal WhatsApp, untracked email chains), enables admin-level analytics and moderation, and supports HIPAA/GDPR audit requirements by keeping message history on-premise.

Audiences

  • Investor: Differentiates Dudoxx HMS as an all-in-one platform rather than a point-of-care tool; captured in-platform messages reduce the risk of regulated data leaving the system.
  • Clinical buyer (doctor/nurse/receptionist): Direct messages to colleagues and patients, group channels per care team, broadcast announcements from admins; pin, archive, mute, and react — same patterns as everyday messaging apps.
  • Developer/partner: REST API under POST /api/messenger/conversations, SSE event stream on the /api/sse endpoint; conversation data lives in ddx_api_main.messenger_* tables; Telegram and WhatsApp bridges are pluggable via BridgeProvider interface.
  • Internal (ops/support): Admin endpoints for conversation listing, message moderation, and analytics; retention cron job (MessageRetentionJob) enforces configurable message lifetime; MessengerEmailNotificationJob sends fallback email alerts for unread messages.

Architecture

The messenger is a NestJS sub-module under src/communication/messenger/. It exposes a single MessengerController backed by a MessengerFacadeService that delegates to eleven focused services. Real-time delivery uses the shared DDX SSE event bus (SseModule) — not a persistent WebSocket server. Each message action publishes per-recipient events on targeted user:{userId} SSE channels, so the browser receives updates via the existing /api/sse stream without a separate connection.

diagram
MessengerController (REST)
    └── MessengerFacadeService
            ├── ConversationService        — create/update/delete/archive/pin/mute
            ├── MessengerMessageService    — send/edit/delete/forward, cursor pagination
            ├── ParticipantService         — join/leave/ban/role-change, ownership transfer
            ├── ReceiptService             — per-message and per-conversation read marks
            ├── ReactionService            — emoji reactions (add/remove)
            ├── MessengerSearchService     — full-text search across conversations
            ├── MessengerUnreadService     — denormalized unread badge counts
            ├── MessengerAttachmentService — file upload to MinIO, group avatar management
            ├── MessengerAdminService      — admin message listing + moderation
            ├── MessengerAnalyticsService  — daily stats, top conversations
            └── BroadcastService           — admin-only broadcast channels
    └── MessengerEventsPublisher
            └── EventBusService (SSE Redis DB1)
    └── Bridges
            ├── WhatsAppBridgeModule       — inbound/outbound relay
            └── TelegramBridgeModule       — inbound/outbound relay
    └── Jobs
            ├── MessengerEmailNotificationJob — fallback email for unread messages
            └── MessageRetentionJob           — configurable message purge

ConversationParticipantGuard enforces membership before any per-conversation operation. The controller is decorated @RbacExempt() — auth is handled at the gateway level; role restrictions are implemented in service layer (e.g., PATIENT cannot create GROUP conversations or forward messages).

Tech Stack & Choices

ComponentChoiceWhy
Persistenceprisma-main (ddx_api_main)Messenger models co-located with users/organizations for single-DB joins; foreign keys to User and Organization enforce tenant integrity
Real-timeSSE via EventBusService (Redis pub/sub)Platform-wide pattern — avoids a separate WebSocket server; acceptable for messaging since delivery latency is <100 ms via Redis
File storageMinIO ({slug}-attachments bucket) via MessengerAttachmentServiceConsistent with platform storage convention; group avatars stored under same bucket
SearchMessengerSearchService (Prisma full-text)Scoped per-user, per-org; no external search engine required at current scale
Email fallbackMessengerEmailNotificationJob (cron, @nestjs/schedule)Participants who miss real-time SSE receive an email digest; uses MailModule
External bridgesWhatsAppBridgeModule, TelegramBridgeModuleOptional opt-in per org via BridgeSettingsService; BridgeProvider interface allows future channels
PaginationCursor-based (cursor + limit query params)Conversation and message lists both use cursor pagination for stable results with concurrent writes

Data Flow

Sending a message (REST → SSE fan-out)

  1. Client (Next.js server action) → POST /api/messenger/conversations/:id/messages with X-API-Key + X-Gateway-* headers.
  2. GatewayAuthGuard validates headers; TenantIsolationInterceptor confirms organizationId.
  3. ConversationParticipantGuard confirms caller is an active participant of the conversation.
  4. MessengerController.sendMessageMessengerFacadeService.sendMessageMessengerMessageService.send.
  5. MessengerMessageService writes MessengerMessage row to ddx_api_main, updates Conversation.lastMessage* denormalization fields in a single transaction.
  6. MessengerEventsPublisher.publishNewMessage iterates active recipientUserIds, calling EventBusService.publish per recipient on channel user:{userId}.
  7. Browser SSE stream receives messenger:new_message event and updates UI without polling.
  8. Unread count badge updated via separate messenger:unread_count event per recipient.

SSE event types published

EventTrigger
messenger:new_messageMessage created
messenger:message_updatedMessage body edited
messenger:message_deletedSoft delete
messenger:typingTyping indicator (client-side; REST endpoint is no-op placeholder)
messenger:read_receiptPOST /conversations/:id/read or per-message read mark
messenger:reactionEmoji reaction added/removed
messenger:conversation_updatedMetadata (name, description, avatar) changed
messenger:participant_joinedNew participant added
messenger:participant_leftLeft, removed, or banned
messenger:unread_countUnread badge refresh
messenger:new_conversationConversation created
messenger:link_previewURL preview resolved

All events are published post-commit (after the Prisma transaction returns) on the per-user user:{userId} channel. See the canonical SSE contract at packages/ddx-sse-contract/ (@ddx/sse-contract, 4.1.0).

Implicated Code

  • ddx-api/src/communication/messenger/messenger.controller.ts:52MessengerController — 30+ REST endpoints covering conversations, messages, participants, receipts, reactions, attachments, admin, and broadcast
  • ddx-api/src/communication/messenger/messenger.module.ts:32 — module wiring: 15 providers, WhatsApp + Telegram bridge sub-modules, Push sub-module, SseModule import
  • ddx-api/src/communication/messenger/services/messenger-facade.service.ts:65MessengerFacadeService — role-check logic (PATIENT guards) + delegation to 11 services
  • ddx-api/src/communication/messenger/publishers/messenger-events.publisher.ts:41MessengerEventsPublisher — fan-out to per-user SSE channels via SSEChannels.user(userId)
  • ddx-api/src/communication/messenger/services/messenger-message.service.ts — core send/edit/delete/forward logic with cursor pagination
  • ddx-api/src/communication/messenger/services/receipt.service.ts — read receipts (per-conversation and per-message)
  • ddx-api/src/communication/messenger/guards/conversation-participant.guard.ts — membership check applied to all conversations/:id routes via @UseGuards(ConversationParticipantGuard)
  • ddx-api/src/communication/messenger/bridges/bridge-settings.service.ts — per-org bridge enable/disable; BridgeProvider interface for pluggable external channels
  • ddx-api/prisma-main/models/messenger.prisma:8Conversation model with last-message denormalization fields (lastMessageId, lastMessageAt, lastMessagePreview, lastMessageSenderId) for O(1) list rendering
  • ddx-api/prisma-main/models/messenger.prisma:49ConversationParticipant with per-user state: unreadCount (denormalized), isMuted, isPinned, isArchived, leftAt (soft removal)

Operational Notes

  • Schema location: Conversation and message models live in prisma-main (ddx_api_main), NOT prisma-com. The prisma-com schema tracks only external outbound channel communications (email, SMS, WhatsApp delivery logs). Confusing the two is a common pitfall — see ddx-api/CLAUDE.md.
  • Typing indicator endpoint: POST /messenger/conversations/:id/typing is intentionally a REST no-op placeholder at messenger.controller.ts:199. Typing events are emitted directly by the client; the REST endpoint exists only for Swagger route registration.
  • Tenant scoping: All conversation queries must carry organizationId; ConversationService.listForUser always filters by { organizationId, userId }. The TenantIsolationInterceptor (position 4 in the global interceptor chain) enforces this at the HTTP layer.
  • Retention: MessageRetentionJob (cron) enforces configurable message lifetime. Soft-delete via deletedAt on Conversation and similar flags on MessengerMessage — admin endpoints can still read moderated content.
  • Bridge opt-in: WhatsApp and Telegram bridges are disabled by default; enabled per-org via BridgeSettingsService. Admin endpoint BridgeAdminController manages credentials.
  • Push notifications: PushModule (sub-module of MessengerModule) handles mobile push alongside SSE for participants who are not actively connected.
  • Email fallback: MessengerEmailNotificationJob uses lastEmailNotifiedAt on ConversationParticipant for dedup — prevents repeated emails for the same unread thread.
  • Validation commands: pnpm tsc:check (ddx-api), pnpm prisma:generate:all if messenger schema changes.
    Messenger — In-App Clinical Messaging — Dudoxx Docs | Dudoxx