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
| Model | Purpose |
|---|---|
chats | thread 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_calls | call_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_calls | one 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_devices | push tokens (apn_token, fcm_token, os) |
companies / agencies | tenant + 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+-statuscallbacks,POST /telnyx/preheat-outbound/:company_id/:agent_id,POST /inbound-outbound-calls/save(post-call persistence;GET /inbound-outbound-calls/:idfalls through tocall_sidlookup). Media-stream WS upgrades on/twilio/media-streamand/telnyx/media-stream(path selects the carrier/codec path inElevenLabsBridgeService). - Socket.IO:
message:send→message:received(room broadcast),call:initiate→ Twilio bridge,notification:push→ APN/FCM. A Firestore listener on/rooms/{room_id}/messageskeeps 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 BullMQadd()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 (viamailError()) is persisted toemail_logs— the only durable prod evidence of a failed send — and callers see the failure.immediate: trueskips 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.)