Anaya Care Docs

Communication (Chat, Notifications, Calls, Announcements, Email)

Part of the Anaya Care product wiki. See 00-overview.md.

Purpose

This module is the platform's real-time and asynchronous communication layer:

  • Chat — 1:1 ("private") and group conversations with attachments, replies, reactions, read/delivery receipts, soft message deletion, and media galleries (apps/backend/src/chat/).
  • Calls — audio/video calls inside private conversations, signaled over the chat WebSocket and carried over LiveKit WebRTC rooms (apps/backend/src/chat/call.service.ts, apps/backend/src/livekit/).
  • Notifications — a unified, registry-driven fan-out hub. Domain modules emit events; listeners resolve recipients; a BullMQ queue dispatches to four channels: in-app (DB + WebSocket), Expo push, Resend email, Twilio SMS (apps/backend/src/notification/).
  • Announcements — business-wide bulletins with priorities, scheduling, acknowledgment tracking, and deadline reminders (apps/backend/src/announcements/).
  • Email — React Email templates sent through Resend, with delivery-status tracking via a Resend webhook (apps/backend/src/emails/, apps/backend/src/common/services/email-sender.service.ts, apps/backend/src/app.controller.ts).

It uses the platform's dual WebSocket architecture: two separate Socket.IO namespaces, /chat (apps/backend/src/chat/chat.gateway.ts:56-59) and /notification (apps/backend/src/notification/notification.gateway.ts:16-19), both authenticated by the same middleware (apps/backend/src/websocket/websocket-auth.service.ts:35-49).

AI chat (the assistant in apps/mobile/app/(ai-chat) and apps/web/.../dashboard/ai-chat) is a separate feature — see 14-ai-features.md.

Entities & Data Model

Conversation — collection conversations (apps/backend/src/chat/entities/conversation.entity.ts)

FieldTypeNotes
businessObjectId → Businessrequired, indexed (line 41-47)
namestringrequired; private conversations are hardcoded 'Private Conversation' (conversation.service.ts:202)
imagestringgroup avatar
typeenum ConversationTypeGROUP (default) or PRIVATE (line 55-61)
createdByObjectId → Userrequired
membersObjectId[] → Userparticipant list
lastMessageObjectId → Messagedenormalized for sidebar
lastMessageAtDatesort key for sidebar
activeCallembedded ActiveCall{type, status, initiatedBy, startedAt, answeredAt} — present only while a call is ringing/active (lines 8-23, 75-85)

Message — collection messages (apps/backend/src/chat/entities/message.entity.ts)

FieldTypeNotes
businessObjectId → Businessrequired, indexed
typeenum MessageTypedefault TEXT; CALL_LOG for inline call entries (chat.service.ts:127)
contentstringoptional if attachments present
senderObjectId → Userrequired
conversationObjectId → Conversation
callLogObjectId → CallLogonly for call-log messages
deliveredTo / readByObjectId[] → Userper-user receipts (unbounded arrays)
attachments{type, url, metadata, blurhash}[]AttachmentType from interfaces/message.interface.ts
replyToembedded {messageId, content, sender, createdAt}snapshot, not a live ref
reactions{emoji, users[]}[]
isDeleted / deletedAt / deletedBysoft deletecontent replaced with "This message was deleted", attachments/reactions cleared (chat.service.ts:761-770)

CallLog — collection call_logs (apps/backend/src/chat/entities/call-log.entity.ts)

FieldTypeNotes
businessObjectId → Businessrequired, indexed
conversationIdObjectId → Conversationindexed with createdAt
callTypeenum CallTypeAUDIO / VIDEO
statusenum CallStatusENDED, DECLINED, MISSED are persisted statuses
initiatedBy / answeredByObjectId → UseransweredBy only when answered
startedAt / answeredAt / endedAtDate
durationnumber (seconds)only for answered calls, measured from answeredAt (call.service.ts:205-208)
endReasonstring'ended', 'declined', 'no_answer', 'disconnected'

Notification — collection notifications (apps/backend/src/notification/entities/notification.entity.ts)

FieldTypeNotes
userToNotifyObjectId → Userrequired
userWhoNotifiedObjectId → Userrequired; system notifications use a dedicated System user (channels/in-app.channel.ts:48-58)
typeenum NotificationTypeType110 registered types (see registry)
linkstringdeep link (web /dashboard/... or mobile route)
isRead / isDeletedbooleansoft delete
metadataMixedtemplate interpolation values
imageUrlstringrich push image
TTLauto-deleted after 90 days via TTL index (line 67-69)

There is no business field on notifications — tenancy is implied by the recipient.

NotificationPreferences — collection notification_preferences (apps/backend/src/notification/entities/notification-preferences.entity.ts)

FieldTypeNotes
userObjectId → Userunique
postNotifications{postLikes, postComments, commentReplies, commentLikes, mentions}per-type toggles, only for community posts (commentLikes defaults false)
enablePushNotifications / enableEmailNotifications / enableSMSNotificationsbooleanper-channel master switches, default true

The per-user unread badge counter lives on the User document (unReadNotificationCount), incremented/decremented by the in-app channel and notification read/delete endpoints, and reconciled daily (see workflows).

Announcement — collection announcements (apps/backend/src/announcements/entities/announcement.entity.ts)

FieldTypeNotes
businessObjectId → Businessrequired, indexed
title / contentstringmax 200 / 5000 chars
priorityenum AnnouncementPrioritydefault MEDIUM; URGENT escalates push behavior
statusenum AnnouncementStatusDRAFTSCHEDULEDPUBLISHEDPAST_DEADLINE
attachmentsimage/video/document array
authorObjectId → User
scheduledAt / deadlineAt / publishedAtDate
reminderTimingsnumber[] (minutes before deadline)drives reminder cron
acknowledgementsCountnumberdenormalized counter
isDeleted / deletedAt / deletedBysoft delete

AnnouncementAcknowledgment — collection announcement_acknowledgments (apps/backend/src/announcements/entities/announcement-acknowledgment.entity.ts)

FieldTypeNotes
businessObjectId → Businessrequired
announcementObjectId → Announcementunique compound index with user (lines 51-54)
userObjectId → User
acknowledgedAtDate

Email tracking

There is no dedicated email collection. Resend delivery events update status fields on existing documents: care-proposal email status via careProposalsService.updateEmailStatus(email_id, status) and user-invite status via usersService.updateUserInviteStatus(email_id, status) (apps/backend/src/app.service.ts:28-44). Statuses: Sent, Delivered, Delivery Delayed, Complained, Bounced, Opened, Clicked (app.service.ts:8-16).

Workflows & State Machines

Chat: connection and rooms

  1. Client connects to the /chat namespace with a JWT in handshake.auth.token; middleware validates token + token version and caches the user on socket.data (websocket-auth.service.ts:35-83).
  2. On connect, the socket joins a personal room chat-user:{userId} and a tenant room business:{businessId}; the userId→socketId mapping is stored in Redis (global hash plus per-business hash) and the business-scoped online-user list is broadcast (chat.gateway.ts:118-149, websocket-auth.service.ts:104-144).
  3. Clients join conversation rooms (conversation:{id}) via JOIN_CONVERSATION / JOIN_CONVERSATIONS / SUBSCRIPTIONS_SYNC; each join performs a membership check canAccessConversation (member-of check, fail-closed on error) and auto-marks all messages delivered (chat.gateway.ts:491-791, chat.service.ts:795-847).

Chat: who can chat with whom

  • Gate: every REST chat endpoint requires BusinessPermission.CHAT_ACCESS (chat.controller.ts:51 etc.). All default roles — Admin, CareProvider, Representative, MedicalProfessional — get CHAT_ACCESS and CALLS_INITIATE (packages/shared/src/constants/default-role-permissions.ts:127-128, 235-236, 304-305, 352-353).
  • 1:1: CREATE_PRIVATE_CONVERSATION (WS) or POST /conversations/private/:userId creates or reuses a 2-member PRIVATE conversation. Dedup is by {type: PRIVATE, members: $all [a,b], $size: 2}not filtered by business (conversation.service.ts:185-209). There is no server-side check that the two users belong to the same business; the conversation's business is the creator's.
  • Group: any user with CHAT_ACCESS can create a group with arbitrary member ids (chat.gateway.ts:846-897, chat.controller.ts:50-69). Only the creator can rename or delete a group; SuperAdmin can delete any conversation (conversation.service.ts:129-183, chat.service.ts:544-585). The creator cannot be removed as a member (conversation.service.ts:70-72).
  • Message deletion: sender can soft-delete own messages; platform roles (SuperAdmin/System via isPlatformRole) can moderate any (chat.service.ts:752-758).
  • Sending: SEND_MESSAGE persists the message, marks it read for the sender, broadcasts RECEIVE_MESSAGE to the conversation room, then emits per-member CONVERSATION_UPDATED with that member's own unread count, and finally emits the chat.message-sent domain event for push delivery to offline members (chat.gateway.ts:197-270). Note: the send handler does not re-verify conversation membership (only room joins do).
  • Unread counts are computed by aggregation over messages (sender ≠ me, readBy ≠ me, not deleted) — no per-conversation counter document (chat.service.ts:294-309, 396-417).

Chat push (offline recipients only)

ChatNotificationListener (apps/backend/src/notification/listeners/chat-notification.listener.ts:29-103) handles chat.message-sent: it removes the sender, filters out users currently in the Redis connected-clients hash, and sends a ChatMessageReceived unified notification with skipInApp: true (chat has its own in-app surface) and a custom push payload (sender as title, 100-char preview as body, deep link /(app)/(chat)/{conversationId}). The ChatMessageReceived registry entry is pushOnly (notification-registry.ts:42-48).

Call flow (LiveKit)

Calls are private-conversation-only, max 2 participants (call.service.ts:75-77, livekit.service.ts:76). Active-call state is the embedded activeCall on the conversation; persisted history is call_logs plus an inline CALL_LOG chat message.

Details: ring timeout is an in-memory setTimeout; stuck RINGING calls are swept to MISSED on server startup and stale activeCalls older than timeout+10s are auto-cleared on the next initiate (call.service.ts:35-54, 79-92). A socket disconnect only ends the user's calls when they have zero remaining sockets in their personal room (multi-device safe; chat.gateway.ts:151-195, 1356-1393). LiveKit credentials default to devkey/devsecret/ws://localhost:7880 when env vars are unset (livekit.service.ts:14-16).

Notification pipeline (the fan-out hub)

The canonical path: domain event → listener (recipient resolution) → NotificationService.sendUnifiedNotification → BullMQ notification queue (unified-notification job, 3 attempts, exponential backoff) → NotificationProcessorNotificationDispatcherService.dispatch → channels (notification.service.ts:416-466, notification.processor.ts:495-562, notification-dispatcher.service.ts:36-156).

Channel eligibility per recipient (notification-dispatcher.service.ts:101-132, 173-207):

  1. Registry definition must enable the channel (NOTIFICATION_REGISTRY, 110 types across 20 categories; presets standard, highPriority, critical (push+email+SMS), withEmail(templateId), pushOnly, silentPushnotification-registry.ts:8-61).
  2. Global config flags (notification.enableInAppNotification|enablePushNotification|enableEmailNotification|enableSMSNotification).
  3. Email/SMS only go to isActive !== false users.
  4. If the definition is userConfigurable, per-user preferences are consulted (shouldSendForChannel: per-type post toggles + per-channel master switch; in-app is never user-suppressible — notification-preferences.service.ts:239-261).
  5. skipInApp payload flag suppresses the in-app channel (used by chat/call pushes).

In-app channel specifics (channels/in-app.channel.ts): system-originated notifications (userWhoNotifiedId null/'system') are attributed to a System user (created on demand); each DB write increments User.unReadNotificationCount with a compensating rollback (notification deleted if the counter update fails); successful writes are pushed over the /notification gateway as NOTIFICATION_SENT to room user-{userId} with the new unread count (notification.gateway.ts:100-144). Counters are reconciled by a daily 2 AM cron plus admin endpoints (counter-reconciliation.service.ts:25-37, notification.controller.ts: counter/*).

Push specifics (channels/push.channel.ts, expo-notification.service.ts): Expo SDK with FCMv1 toggle, chunked sends, receipt processing after a delay, and automatic removal of invalid tokens (DeviceNotRegistered etc., expo-notification.service.ts:401-533). Batches >50 users are re-queued as batch-push jobs. Android channel + iOS thread ids come from resolveNotificationThread in @anaya/shared.

Other queue job types (notification.processor.ts:77-90): care-proposal (status-based recipient resolution with reviewer-targeted links, see handleCareProposalNotification lines 100-363), shift-response (notifies all BUSINESS_MANAGEMENT_ROLES managers), scheduled-push, batch-push.

Specialized senders that compose onto the same pipeline: ShiftNotificationService (assignment, response, reassignment, cancellation, modification, pickup, clock events, bulk ops, reflection reminders — shift-notification.service.ts), PostNotificationService (Redis-batched like notifications, 5-min window, max 10 per batch — post-notification.service.ts:34-41).

Announcement lifecycle

  • Create/update/publish/delete require ANNOUNCEMENTS_MANAGE; stats and the admin list require ANNOUNCEMENTS_VIEW; the feed, counts, detail, and acknowledge endpoints are open to all authenticated users in the business (announcements.controller.ts:45-181).
  • On publish, announcement.published is emitted (announcements.service.ts:968-1001); AnnouncementNotificationService sends an AnnouncementPublished unified notification to all active users excluding the author — the recipient query is {isActive: true} with no business filter (announcement-notification.service.ts:106-114). Priority shapes the push (URGENT → high priority, time-sensitive interruption, 🚨 title).
  • Crons (tasks/announcement.tasks.ts): publish due SCHEDULED announcements every minute; mark PAST_DEADLINE every 5 minutes.
  • Deadline reminders (services/announcement-monitoring.service.ts:50-157): every 5 minutes, for published announcements with deadlineAt within 24h and reminderTimings, unacknowledged active users of the announcement's business get an AnnouncementDeadlineReminder (sent as the System user), deduplicated per (announcement, timing, user) via Redis keys with a 24h TTL (services/announcement-reminder-tracking.service.ts).
  • Acknowledgment: allowed on PUBLISHED and PAST_DEADLINE; idempotent (unique index + early return); increments acknowledgementsCount (announcements.service.ts:654-712).

Transactional email (Resend) & webhooks

  • All email flows through EmailSenderService (apps/backend/src/common/services/email-sender.service.ts): sliding-window rate limit of 8/sec with 429 retry/backoff; sendTransactional (auth/OTP/password flows — bypasses the feature flag) vs sendOperational (notification-channel and domain emails — respects ENABLE_EMAIL_NOTIFICATION). Subjects are prefixed [STAGING] in staging (doSend, line 121; common/utils/runtime-environment.ts:26-32).
  • Rich domain templates are React Email components in apps/backend/src/emails/ (EmailVerification, MagicLink, PasswordResetRequest/Success, InviteUserEmail, RepresentativeWelcomeEmail, CareProposalEmail/Revised, IdVerification Submitted/Approved/Rejected, Admin alerts/digests) and are sent directly by their owning services — not through the notification dispatcher (channels/email-template.resolver.tsx:40-42).
  • Notification-channel emails use one generic branded layout (interpolated title/body + optional CTA whose relative link is absolutized with FRONTEND_URL); the registry templateId is currently ignored by the resolver beyond gating (email-template.resolver.tsx:49-62). From-address: Anaya Care <donotreply@anayacare.com> (channels/email.channel.ts:10).
  • Webhook: POST /webhook/resend verifies the Svix signature (HMAC over id.timestamp.rawBody, 5-minute replay window) only when RESEND_WEBHOOK_SECRET is configured; otherwise events are accepted unauthenticated (app.controller.ts:29-73). Events map to email statuses and update care-proposal / user-invite records matched by subject prefix ("Care Proposal for…", "You have been invited to join Anaya Care…") (app.service.ts:28-83).
  • SMS goes through SmsSenderService (Twilio; sendOperational respects ENABLE_SMS_NOTIFICATION; Twilio Verify for OTP; dev bypass code + +1555010XXXX test numbers — common/services/sms-sender.service.ts:44-75).

Business Rules & Constraints

  • Conversations, messages, call logs, announcements, and acknowledgments are tenant-scoped via a required business field; Notification documents are not (recipient-scoped only) (entities/*.ts).
  • Only PRIVATE conversations support calls; one active call per conversation; LiveKit rooms cap at 2 participants and auto-close 60s after the last participant leaves (call.service.ts:75-91, livekit.service.ts:71-84).
  • Ring timeout is 45 seconds (packages/shared/src/constants/call.constants.ts:2); LiveKit access tokens expire after 10 minutes (livekit.service.ts:49).
  • Group deletion: creator or SuperAdmin only; private conversation deletion: any member; deleting a conversation hard-deletes all its messages (chat.service.ts:544-602).
  • Message deletion is soft and irreversible in UI terms — content replaced, attachments/reactions wiped; only the sender or a platform role may delete (chat.service.ts:752-770).
  • A message must contain content or at least one attachment (chat.service.ts:48-53).
  • Notifications expire automatically after 90 days (TTL index, notification.entity.ts:67-69).
  • Chat pushes go only to recipients with no live WebSocket connection; in-app notification rows are never created for chat (skipInApp, chat-notification.listener.ts:42-56, 78).
  • Email/SMS channels skip inactive users; the email channel additionally requires a registry templateId and a recipient email on file (notification-dispatcher.service.ts:112-115, email.channel.ts:36-50).
  • User preferences only suppress userConfigurable notification types; in-app delivery cannot be turned off per user (notification-dispatcher.service.ts:173-187, notification-preferences.service.ts:249-260).
  • Announcements: title ≤ 200 chars, content ≤ 5000; acknowledge only PUBLISHED/PAST_DEADLINE; one acknowledgment per user per announcement (unique index) (announcement.entity.ts:49-53, announcements.service.ts:665-688).
  • Marking a notification read decrements the user counter only if it was unread and the counter is > 0; mark-all-as-read zeroes the counter (notification.service.ts:218-288).
  • Push tokens are stored as User.expoPushTokens[]; invalid tokens are pruned automatically from Expo receipts/tickets (expo-notification.service.ts:467-533).

Surfaces (Web & Mobile)

Web (apps/web — Admin/Owner/CareManager dashboard)

  • Socket bootstrap: components/client-layout.tsx:159-260 opens both namespaces (${NEXT_PUBLIC_BACKEND_URL}/chat and /notification) once a session token exists; tracks online users, hydrates the conversation store for the sidebar unread badge, plays a chat sound for new messages in non-active conversations (frontend-only rule), and on NOTIFICATION_SENT invalidates React Query caches (including care-proposals for care-proposal types), plays a notification sound, and shows a toast.
  • Chat UI: app/(app)/(admin)/dashboard/chat and chat/[chatId] with components in components/chat/ (chat-context.tsx, chat-panel.tsx, use-chat-socket-manager.tsx). Calls are supported via hooks/use-livekit-call.ts.
  • Notifications: dropdown in the header (components/notification-dropdown.tsx), preference modal (components/user-profile/modals/notification-preferences-modal.tsx).
  • Announcements: admin CRUD at app/(app)/(admin)/dashboard/announcements (list, add, [id], [id]/edit) — gated server-side by ANNOUNCEMENTS_MANAGE/ANNOUNCEMENTS_VIEW permissions.

Mobile (apps/mobile — CareProviders, Representatives/Families)

  • Socket bootstrap: contexts/socket-context.tsx:58-70 opens both namespaces with the auth token; global listeners update the chat store, play sounds (suppressed for own messages and the active conversation — frontend-only), and handle CALL_RINGING/CALL_STATUS_CHANGED via a Zustand call store (ignores own calls and concurrent ringing — frontend-only, lines 122-140).
  • Chat screens: list/threads at app/(app)/(tabs)/chat/ and full conversation experience at app/(conversation)/[id]/index (messages), call, incoming-call, media, members, settings.
  • Announcements: feed with All/Unread/priority filters at app/(announcements)/index.tsx (uses unacknowledgedOnly param) and detail/acknowledge at [announcementId].tsx.
  • Notifications: list + settings at app/(app)/(tabs)/notifications/ and app/(shared)/notifications/.
  • Push setup: services/notificationService.ts requests permissions, registers Android channels from constants/notification-channels, and sets a foreground handler that suppresses chat pushes for the currently open conversation (lines 8-32). Tokens are synced to POST /users/:id/expo-push-token and removed on logout (lib/push-token-api.ts). Tap handling (contexts/notification-context.tsx:81-116): incoming_call data hydrates the call store and navigates to the incoming-call screen; jobPostingId deep-links to the job; a generic data.screen branch exists but is empty (no-op) (lines 114-115).
  • AI chat lives at app/(ai-chat) — documented in 14-ai-features.md.

Cross-Module Dependencies

This module is the fan-out hub. Producers and their entry points into the pipeline:

Producer moduleMechanismEvents / callsListener / handler
Care proposals (src/care-proposals) → 03-care-proposals.mdeventcare-proposal.status-changed, care-proposal.authorship-transferredlisteners/notification.listener.ts:45-93 → queued care-proposal job (notification.processor.ts:100-363)
Clients / caregiver assignments (src/clients) → 01-clients.mdeventcaregiver.invitation-sent, caregiver.status-updated, caregiver.removed, careprovider.separated, careprovider.separation-scheduled, careprovider.reinstatedlisteners/notification.listener.ts:95-302
Incident reports (src/incident-reports) → 09-incidents-and-alerts.mdeventincident-report.submitted, incident-report.status-changed, incident-report.shared-with-familylisteners/notification.listener.ts:306-410 (permission-based recipients: INCIDENT_REPORTS_VIEW_ALL)
Emergency alerts (src/emergency-alerts) → 09-incidents-and-alerts.mdeventEMERGENCY_ALERT_EVENTlisteners/emergency-alert-notification.listener.ts (role-based recipients: MANAGEMENT_ROLES + family reps)
Requests (src/requests)eventrequest.created, request.status-changed, request.comment-addedlisteners/request-notification.listener.ts (MANAGEMENT_ROLES)
Medication inventoryeventTHRESHOLD_BREACH_EVENT, MEDICATION_TASK_COMPLETED_EVENTlisteners/threshold-breach-notification.listener.ts, listeners/medication-stock-notification.listener.ts (MANAGEMENT_ROLES)
Wound careeventwound-care.assessment-created, wound-care.analysis-completed, wound-care.condition-worseninglisteners/wound-care-notification.listener.ts (BUSINESS_MANAGEMENT_ROLES)
Users (src/users)eventuser.createdlisteners/user-created.listener.ts
Chat (this module)eventchat.message-sent, chat.incoming-call, chat.missed-calllisteners/chat-notification.listener.ts, listeners/call-notification.listener.ts
Announcements (this module)eventannouncement.publishedannouncement-notification.service.ts:20
Shifts (src/shifts, src/shift-handovers) → 05-scheduling-and-shifts.mddirect callShiftNotificationService.*, queueShiftResponseNotification, sendUnifiedNotificationshifts.service.ts, shifts.processor.ts, shift-handovers.service.ts
Job postings (src/job-postings) → 06-job-postings.mddirect callsendUnifiedNotificationservices/job-postings.service.ts, services/job-applications.service.ts
Health observations, appointments, task comments, birthdays, postsdirect callsendUnifiedNotification / PostNotificationServicehealth-observations.service.ts, clients/services/appointment-monitoring.service.ts, clients/services/task-submission-comments.service.ts, posts/tasks/birthday.tasks.ts
AI orchestrators (care plans, meals, tasks) → 14-ai-features.mddirect call + WS progresssendUnifiedNotification; NotificationGateway.emitCarePlanProgress / emitCareProviderTasksProgress (notification.gateway.ts:186-213)ai/agents/*-orchestrator.service.ts, clients/processors/*

Open Questions & Gaps

  1. Cross-tenant announcement fan-out. AnnouncementNotificationService.getAllActiveUserIds queries {isActive: true} with no business filter, so publishing an announcement notifies every active user on the platform, across all tenants (announcement-notification.service.ts:106-114; duplicate helper at announcements.service.ts:956-964). The deadline-reminder path is business-scoped (announcement-monitoring.service.ts:97-104) — the two recipient strategies are inconsistent. Cannot determine from code whether the global fan-out is intentional (e.g., assuming single-tenant deployments).
  2. Inconsistent recipient selection: permission-based vs deprecated role constants. Incident-report notifications resolve recipients via PermissionService.getUserIdsWithPermission(businessId, INCIDENT_REPORTS_VIEW_ALL) (notification.listener.ts:313-317), while emergency alerts use the deprecated MANAGEMENT_ROLES role query (emergency-alert-notification.listener.ts:40-46; MANAGEMENT_ROLES is marked @deprecated in packages/shared/src/enums/user-role.ts:47-53 and includes platform roles SuperAdmin/System, which the business filter silently excludes). Requests, medication-stock, and threshold-breach listeners also use MANAGEMENT_ROLES; wound-care and shift-response use BUSINESS_MANAGEMENT_ROLES. Users with Custom roles holding the relevant permission receive incident notifications but not emergency alerts, request, or stock alerts.
  3. critical channel preset enables email without a templateId, so those emails never send. CHANNELS.critical sets email: { enabled: true } with no templateId (notification-registry.ts:23-34); EmailChannel.send returns early when templateId is missing (email.channel.ts:36-41). EmergencyCallInitiated and WoundConditionCritical (registry lines 848-889) therefore deliver push + SMS but silently skip email despite the registry claiming email is enabled.
  4. Unscoped admin-ish notification endpoint. POST /notification/push/send-to-users is guarded only by JwtAuthGuard + throttling — any authenticated user can push arbitrary title/body to arbitrary userIds, including users of other businesses (notification.controller.ts:116-163). Same for POST /notification/push/send variants and queue-inspection endpoints (queue/status, worker/health) which expose operational data to any user.
  5. Chat REST endpoints skip membership and tenancy checks. GET /conversations/groups/:groupId, GET /:conversationId/media, GET /:conversationId/call-logs, GET /messages/:messageId, PUT /:conversationId/mark-read|mark-delivered, and PUT/DELETE group members validate only CHAT_ACCESS, not that the caller is a member of (or in the same business as) the conversation (chat.controller.ts:80-108, 201-256, 326-336, 383-397). The WS SEND_MESSAGE handler likewise does not verify membership before persisting/broadcasting (chat.gateway.ts:197-231) — only room joins do (chat.gateway.ts:514-529).
  6. Private conversations are not tenant-checked. createPrivateConversation accepts any target userId and dedups across all businesses (conversation.service.ts:185-209); nothing prevents a private conversation between users of different businesses. The conversation's business is silently set to the creator's.
  7. Shared connected-clients registry across both gateways can corrupt presence state. ws:connected_clients maps userId→socketId and is written by both the chat and notification gateways (chat.gateway.ts:124-130, notification.gateway.ts:46-52) — last write wins, and removeConnectedClient deletes the user entirely when either socket disconnects (websocket-auth.service.ts:150-184). Consequences: (a) ChatNotificationListener's "offline" check can misclassify users whose notification socket disconnected last; (b) getSocketIdForUser used for cross-namespace emits (chat.gateway.ts:817-824, 872-880) may return a socketId from the other namespace, where the emit silently goes nowhere. The whole hash is also read-modify-written without atomicity (race under concurrent connects).
  8. Resend webhook is unauthenticated when RESEND_WEBHOOK_SECRET is unset, and email-status correlation matches on hardcoded subject prefixes (app.controller.ts:37-70, app.service.ts:31-36) — the staging [STAGING] subject prefix (email-sender.service.ts:121) breaks startsWith matching, so staging email tracking silently no-ops. Cannot determine from code whether this is accepted behavior.
  9. read endpoint lacks an ownership check. NotificationService.read looks up by _id only (notification.service.ts:219-224) — any authenticated user can mark any user's notification read (and decrement their own counter, desyncing both). softDelete correctly filters by userToNotify (line 291-295).
  10. In-memory call ring timeouts are not multi-instance safe. ringTimeouts is a per-process Map (call.service.ts:21); with multiple backend replicas, the accept/decline may land on a different instance than the one holding the timer (mitigated only by the startup sweep and stale-call auto-clear).
  11. Notification listeners swallow all errors (every handler catches and logs without rethrowing — e.g., notification.listener.ts:59-64), so recipient-resolution failures are invisible to the emitting workflow; only post-queue failures get BullMQ retries.
  12. Registry templateIds are decorative. EmailTemplateResolver.resolve ignores the templateId argument and always renders the same generic layout (email-template.resolver.tsx:49-62), so withEmail('care-proposal-status') vs withEmail('shift-assigned') differ only in interpolated text.
  13. Event payload key mismatch: the chat gateway emits chat.message-sent with key businessId (chat.gateway.ts:256) while ChatMessageSentEvent declares business (chat-notification.listener.ts:14); the field is unused so it's benign today, but the type is wrong.
  14. Per-type notification preferences only exist for community posts (notification-preferences.service.ts:89-107); the preferences UI category model implied by the registry's 20 categories has no backend storage — users can only toggle whole channels.
  15. getUserConversationsWithUnread is sent on every room join with hasMore: false (chat.gateway.ts:553-570), unpaginated — conflicts with the cursor-paginated GET_CONVERSATIONS path and may overwrite client pagination state. Intent cannot be determined from code.

On this page