Skip to main content

ms-communication — Chats, Calls & Notifications

Real-time messaging and voice. A Nodevel service on MongoDB, with Firestore as the live message store.

  • Stack: Express/Nodevel, Mongoose/MongoDB, Socket.IO (+ Redis adapter), Firebase/Firestore.
  • Port: 8080.

Responsibilities

  • Messaging — text chat over Socket.IO + Firestore; Slack thread sync; message persistence.
  • Voice/video calls — Twilio and Telnyx PSTN ↔ ElevenLabs media-stream bridges, WebRTC (LiveKit) token relay for widget AI calls, recording links, inbound/outbound automation, post-call webhooks. See Telephony.
  • Notifications — APN (iOS VoIP push), FCM (Android), email (SendGrid/ nodemailer).
  • Multi-tenant routing — associate users/teams/rooms with companies.
  • Webhooks — Twilio call events; Slack event subscriptions.

Boot flow

Standard Nodevel (index.js → Application.run() → app/hooks.js). boot() loads Redis, Auth, Mongo, Cron, Logger, Mail, Permissions, Firebase, Firestore, Slack, APN, FCM, and an ElevenLabs voice bridge.

Directory map

app/
├── models/ chats, inbound_outbound_calls, screened_calls, user_devices,
│ companies, agencies
├── services/ ApnService, FcmService, FirestoreService, MailService, SlackService,
│ MessagingService, SocketService, ElevenLabsBridgeService,
│ PostCallWebhookService, RecordingLinkService
├── controllers/ MessageController (large — chat logic), CallsController,
│ InboundCallController (incl. keypress screening gate),
│ OutboundCallController (incl. Twilio AMD callback),
│ InboundOutboundCallController, TelnyxInboundCallController,
│ TelnyxOutboundCallController (incl. Telnyx AMD callback),
│ SlackController
├── helpers/ TelnyxHelper (TeXML), AudioTranscodeHelper (mu-law + A-law),
│ FirstMessageHelper (continuation opener via ms-ai, 20s timeout), …
├── jobs/ cron jobs (e.g. recording merge)
├── keys/ APN cert (.p8), Firebase service account
└── routes/ api.js (Telnyx routes are declared in the controllers' routes())

Data models

ModelPurpose
chatsthread metadata with embedded messages[] (sender, text, status, timestamp, slack_thread_id). Sticky has_bot_message / has_visitor_message flags stamped on write (never reset by "New chat" — backend daily-brief chat counts read them). widget_chat_starting_url = page the visitor was on when the conversation started (re-stamped on "New chat"); leadconnector_starting_url_note_id = GHL note id once posted (backend writes it, ms-comm clears on reset-chat).
inbound_outbound_callscall_sid, status, duration, recording_url, company_id, provider (twilio/telnyx), sentiment, voicemail, call_outcome, summary, post_call_webhook_sent_at, post_call_tags_applied, ghl_recording_state
screened_callsone row per inbound call turned away by the keypress screening gate (call_sid unique upsert, reason: no_input|wrong_digit, TTL 90d). Deliberately separate from inbound_outbound_calls so no call-count consumer changes.
user_devicespush tokens (apn_token, fcm_token, os)
companies / agenciestenant + Twilio/Slack config

Shared-collection indexes change in lock-step. chats and user_sessions schemas are declared in multiple services with autoIndex — a deploy of any one service with a divergent declaration rebuilds (or conflict-fails) the index for everyone. Recent examples: the { company_id, is_viewed, updated_at } index on user_sessions was removed, and the chats transcript-email sweep index was redefined as transcript_email_sweep_v2 ({ transcript_email_sent, created_at, updated_at }, partial on transcript_email_sent: false) — both must match the backend/ms-sessions declarations exactly.

Endpoints & socket events

  • HTTP: health, POST /auth/login, POST /webhooks/twilio/inbound, Slack OAuth callback + POST /integrations/slack/events (raw-body signature verification).
  • Telephony: POST /twilio/{inbound,outbound}-voice/:company_id, POST /twilio/inbound-dial-fallback/:company_id (routing-priority chain), POST /twilio/inbound-screen/:company_id (keypress screening gate — stateless, digit rides ?d=, fail-open; see Telephony), POST /twilio/amd-status/:company_id + POST /telnyx/amd-status/:company_id (async AMD verdicts, always 204), POST /telnyx/{inbound,outbound}-voice/:company_id + -status callbacks, POST /telnyx/preheat-outbound/:company_id/:agent_id, POST /inbound-outbound-calls/save (post-call persistence; GET /inbound-outbound-calls/:id falls through to call_sid lookup). Media-stream WS upgrades on /twilio/media-stream and /telnyx/media-stream (path selects the carrier/codec path in ElevenLabsBridgeService).
  • Socket.IO: message:sendmessage:received (room broadcast), call:initiate → Twilio bridge, notification:push → APN/FCM. A Firestore listener on /rooms/{room_id}/messages keeps clients in sync.

Integrations

Twilio (calls/recording), Telnyx (TeXML second carrier), APN (iOS VoIP push), Firebase Admin (FCM + Firestore), SendGrid/nodemailer (email), Slack Web API, ElevenLabs (Convai voice — websocket bridge + WebRTC/LiveKit tokens), AWS S3 (call recordings).

Email path

MailService.send() no longer sends directly by default — it renders the EJS template locally and enqueues the pre-rendered HTML onto the backend's BullMQ email queue (the backend worker also accepts template jobs; pre-rendered { html, subject, to, … } jobs are supported for exactly this producer):

  • Queue Redis resolves EMAIL_QUEUE_REDIS_HOST → REDIS_HOST → localhost (app/configs/redis.js) — the DEV override points at the backend's Redis.
  • queueWithTimeout(payload, 5000) races BullMQ add() against a 5s timer (add() hangs while Redis is down); on timeout/error the send falls back to direct SMTP/SendGrid rather than losing the email. A late add() after the race is dropped to avoid duplicates.
  • Smtp() / sendGrid() now await and rethrow: the full nodemailer/ SendGrid error object (via mailError()) is persisted to email_logs — the only durable prod evidence of a failed send — and callers see the failure.
  • immediate: true skips the queue entirely.

One template shell (2026-08-27, d7ef3b8 / backend 142711093). app/views/templates/common/GENERAL.html is the only base (EMPTY.html stays for raw sends); MINIMAL.html and WHITE_LABEL.html were deleted in both repos and the 36 bodies are content-only fragments injected as body. common/_brand-logo.html decides the mark: white-label + agency logo → agency logo; white-label without logo → agency name text (a sub-account must never see the KK mark); otherwise the KK lockup with a prefers-color-scheme dark swap. Assets load from <base_url>/emails/* — ms-comm defaults base_url to Config.app('main_backend_url') because it has no /emails route. Preview locally with scripts/preview-emails.js.preview/*.html. MailService.isArchivedRecipient(to, company_id) (mirrored from backend) runs at the top of send(): an explicit live company_id wins; otherwise all recipient addresses must resolve to archived users for the send to be blocked — one unknown address (lead, applicant, alias) keeps it alive. Blocks password resets too.

Transcript email sweep v3 (97edc74 / backend 7abd7c124): keyed on last_message_at, index transcript_email_sweep_v3 = {transcript_email_sent, created_at, last_message_at} partial on false, declared here and in backend lock-step; the claim is the timestamp transcript_email_processing_at (the old boolean was undeclared → dropped by strict mode → all four schedular pods raced), released on every exit path, STALE_CLAIM_MS = 15 min. Root cause of the hours-late batching was FactExtractionTriggerJob bumping updated_at every ~10 min; its no_person_id outcome is now a bounded wait (PERSON_WAIT_MS = 6 h) with no attempt spent. Call transcript emails lead with the ElevenLabs call_summary_title and render the summary above the transcript; the claim is gated on record.summary so the analysis-blind finalize pass doesn't burn it, and backend CallTranscriptEmailFallbackJob sends without a summary after 5 min.

Realtime

Socket.IO 4.8 with @socket.io/redis-adapter for multi-pod fan-out; SocketService mirrors message state between Socket.IO and Firestore.

Cross-service

Receives auth/tenant config from backend; pushes call-completion and read receipts back via webhook; queries ms-sessions presence to confirm a participant is live. Twilio in-progress on an outbound AI call fire-and-forgets a live-call push to backend POST /internal/knox/notifications/dispatch (type ai-call-live, dedup key per call SID) so the push governance pipeline applies. See Cross-Service Communication.

Deployment

Docker (Node 22 Alpine, port 8080) → ECR → EKS. HTTP pods scale on CPU; no separate worker pool (jobs are cron-based; the exactly-once post-call webhook fallback deliberately lives in the single-pod backend, not here). buildspec.yml builds, pushes, and rolls out. Secrets (TWILIO_*, SLACK_*, FIREBASE_KEY, TELNYX_MEDIA_STREAM_PATH, TELNYX_PUBLIC_WS_URL, RECORDING_LINK_SECRET, EMAIL_QUEUE_REDIS_HOST — DEV override for the backend email queue) come from the cluster. (AMD tuning env AMD_ENABLED/AMD_TIMEOUT_SECONDS/AMD_SPEECH_THRESHOLD_MS lives in ms-ai, which appends the dial params; ms-comm only receives the callbacks.)