Messenger — In-App Clinical Messaging
Audiences: doctor, clinical-buyer, developer
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/sseendpoint; conversation data lives inddx_api_main.messenger_*tables; Telegram and WhatsApp bridges are pluggable viaBridgeProviderinterface. - Internal (ops/support): Admin endpoints for conversation listing, message moderation, and analytics; retention cron job (
MessageRetentionJob) enforces configurable message lifetime;MessengerEmailNotificationJobsends 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.
MessengerController (REST) → MessengerFacadeService, which owns:
| Service | Responsibility |
|---|---|
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 |
MessengerController also owns three sibling branches:
| Branch | Member | Responsibility |
|---|---|---|
MessengerEventsPublisher | EventBusService | SSE Redis DB1 |
| Bridges | WhatsAppBridgeModule | inbound/outbound relay |
| Bridges | TelegramBridgeModule | inbound/outbound relay |
| Jobs | MessengerEmailNotificationJob | fallback email for unread messages |
| Jobs | 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
| Component | Choice | Why |
|---|---|---|
| Persistence | prisma-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-time | SSE 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 storage | MinIO ({slug}-attachments bucket) via MessengerAttachmentService | Consistent with platform storage convention; group avatars stored under same bucket |
| Search | MessengerSearchService (Prisma full-text) | Scoped per-user, per-org; no external search engine required at current scale |
| Email fallback | MessengerEmailNotificationJob (cron, @nestjs/schedule) | Participants who miss real-time SSE receive an email digest; uses MailModule |
| External bridges | WhatsAppBridgeModule, TelegramBridgeModule | Optional opt-in per org via BridgeSettingsService; BridgeProvider interface allows future channels |
| Pagination | Cursor-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)
- Client (Next.js server action) →
POST /api/messenger/conversations/:id/messageswithX-API-Key+X-Gateway-*headers. GatewayAuthGuardvalidates headers;TenantIsolationInterceptorconfirmsorganizationId.ConversationParticipantGuardconfirms caller is an active participant of the conversation.MessengerController.sendMessage→MessengerFacadeService.sendMessage→MessengerMessageService.send.MessengerMessageServicewritesMessengerMessagerow toddx_api_main, updatesConversation.lastMessage*denormalization fields in a single transaction.MessengerEventsPublisher.publishNewMessageiterates activerecipientUserIds, callingEventBusService.publishper recipient on channeluser:{userId}.- Browser SSE stream receives
messenger:new_messageevent and updates UI without polling. - Unread count badge updated via separate
messenger:unread_countevent per recipient.
SSE event types published
| Event | Trigger |
|---|---|
messenger:new_message | Message created |
messenger:message_updated | Message body edited |
messenger:message_deleted | Soft delete |
messenger:typing | Typing indicator (client-side; REST endpoint is no-op placeholder) |
messenger:read_receipt | POST /conversations/:id/read or per-message read mark |
messenger:reaction | Emoji reaction added/removed |
messenger:conversation_updated | Metadata (name, description, avatar) changed |
messenger:participant_joined | New participant added |
messenger:participant_left | Left, removed, or banned |
messenger:unread_count | Unread badge refresh |
messenger:new_conversation | Conversation created |
messenger:link_preview | URL 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:52—MessengerController— 30+ REST endpoints covering conversations, messages, participants, receipts, reactions, attachments, admin, and broadcastddx-api/src/communication/messenger/messenger.module.ts:32— module wiring: 15 providers, WhatsApp + Telegram bridge sub-modules, Push sub-module, SseModule importddx-api/src/communication/messenger/services/messenger-facade.service.ts:65—MessengerFacadeService— role-check logic (PATIENT guards) + delegation to 11 servicesddx-api/src/communication/messenger/publishers/messenger-events.publisher.ts:41—MessengerEventsPublisher— fan-out to per-user SSE channels viaSSEChannels.user(userId)ddx-api/src/communication/messenger/services/messenger-message.service.ts— core send/edit/delete/forward logic with cursor paginationddx-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 allconversations/:idroutes via@UseGuards(ConversationParticipantGuard)ddx-api/src/communication/messenger/bridges/bridge-settings.service.ts— per-org bridge enable/disable;BridgeProviderinterface for pluggable external channelsddx-api/prisma-main/models/messenger.prisma:8—Conversationmodel with last-message denormalization fields (lastMessageId,lastMessageAt,lastMessagePreview,lastMessageSenderId) for O(1) list renderingddx-api/prisma-main/models/messenger.prisma:49—ConversationParticipantwith 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), NOTprisma-com. Theprisma-comschema tracks only external outbound channel communications (email, SMS, WhatsApp delivery logs). Confusing the two is a common pitfall — seeddx-api/CLAUDE.md. - Typing indicator endpoint:
POST /messenger/conversations/:id/typingis intentionally a REST no-op placeholder atmessenger.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.listForUseralways filters by{ organizationId, userId }. TheTenantIsolationInterceptor(position 4 in the global interceptor chain) enforces this at the HTTP layer. - Retention:
MessageRetentionJob(cron) enforces configurable message lifetime. Soft-delete viadeletedAtonConversationand similar flags onMessengerMessage— admin endpoints can still read moderated content. - Bridge opt-in: WhatsApp and Telegram bridges are disabled by default; enabled per-org via
BridgeSettingsService. Admin endpointBridgeAdminControllermanages credentials. - Push notifications:
PushModule(sub-module of MessengerModule) handles mobile push alongside SSE for participants who are not actively connected. - Email fallback:
MessengerEmailNotificationJobuseslastEmailNotifiedAtonConversationParticipantfor dedup — prevents repeated emails for the same unread thread. - Validation commands:
pnpm tsc:check(ddx-api),pnpm prisma:generate:allif messenger schema changes.
Related Topics
- SSE Event Engine — EventBusService, channel naming, Redis pub/sub
- Prisma 7 Schemas — prisma-main vs prisma-com split
- Email / SMS / Notifications — external outbound channel delivery (prisma-com)