Notifications
Audience: Sales · Support · QA · Management · Engineering · Where in app: Settings → Notifications (company) and per-user profile preferences · Plan availability: All plans (Slack and some report emails may be plan-gated — verify)
Notifications are everything that alerts the team when something happens with a visitor: a new chat, an incoming call, a missed call, a hot lead, a new form submission. The same underlying event can fan out to several channels at once — a banner inside the app, a native OS toast on the desktop app, a push to a phone, an email, and a message in Slack — depending on what the company and the individual team member have turned on.
What it does
- Tells agents in real time that a visitor needs them (new chat, incoming call, transfer/takeover request).
- Tells managers about outcomes and signals (hot lead identified, booking confirmed, daily recap, periodic reports).
- Delivers across five channels: in-app, desktop toast, mobile/desktop push, email, and Slack.
- Records in-app notifications so they persist in the bell/drawer and deep-link to the source (chat, call, booking, session).
- Self-heals delivery: stale push tokens are automatically removed when the provider reports them dead.
How it works
A notification is created in ms-communication and then dispatched to whichever channels are enabled:
- In-app record.
NotificationHelper.sendNotification()writes a row to thenotificationscollection (NotificationModel) withcompany_id, optionaluser_id,title,message,type, and arelationsobject (a pointer such as{ chat_id, user_session_id }or{ call_id }). Therelationsobject is what makes a notification clickable — the UI uses it to deep-link to the right screen. - Real-time fan-out over socket.io. The server emits a
new-notificationevent to the company room (io.to(company_id).emit('new-notification', …)), so every connected agent dashboard and desktop app updates its bell count and toasts immediately. For a new visitor message the payload also carriesnotification_type: 'new-user-message'. - Push. For mobile/desktop, a push is sent via Firebase Cloud Messaging (
FcmService) and/or Apple Push Notification service (ApnService). VoIP-style pushes (for incoming calls) go through APNs PushKit so the device can ring even when the app is backgrounded. - Email. Transcript emails, missed-call emails, recap/report emails are rendered from server-side EJS templates and sent via
MailService(SMTP or SendGrid). - Slack. If the company has connected Slack, the relevant events post to the chosen channel via
SlackService. Slack is bidirectional for chat — see Live Chat.
Notification types (in-app)
NotificationHelper.NOTIFICATION_TYPES defines the in-app types currently emitted by ms-communication:
| Type constant | Value | Fires when |
|---|---|---|
NEW_SESSION | new-session | A new visitor session starts |
NEW_MESSAGE | new-message | A visitor sends their first message in a chat |
NEW_CALL | new-call | A new incoming call arrives |
MISSED_CALL | missed-call | A visitor tried to call but no agent picked up |
CHAT_TRANSFER | chat-transfer | A chat is transferred / takeover requested |
AI_LEAD | ai-lead | The AI surfaces/qualifies a lead |
The nox-knowledge-base lists additional conceptual notification reasons (booking confirmed, hot lead identified, lead score climbed, new form submission, new webhook event, team member joined, AI credits low/depleted). These are surfaced across the product; the exact set persisted by NotificationModel is the enum above — treat anything beyond it as verify for the precise stored type value.
Configuration & options
Company-level toggles (companies.notifications.*)
| Field | What it controls |
|---|---|
chat_transcript | After a chat ends, email the full transcript to chat_transcript_emails_to[] |
chat_transcript_emails_to[] | Recipient list for transcript emails |
call | Notify on a new incoming call |
missed_call | Email when a visitor tried to call but nobody picked up |
reports_email.enabled / reports_email.frequency | Periodic stats email — daily / weekly / monthly |
daily_recap_email.enabled | Yesterday's summary every morning |
daily_recap_email.last_sent_at | Timestamp of the last recap send (used to avoid duplicates; Nox can read it) |
slack_new_visitor | Post to Slack on every new visitor session |
slack_hot_lead | Post to Slack when a lead score crosses the hot threshold |
A separate AI voice missed-call email fires when an inbound AI call ends without an action (e.g. no booking). It is its own notification type, distinct from
missed_call. See Calls.
Per-user preferences
Each team member has their own notification preferences on their user record:
- Email on new chat
- Email on new call
- Push on new chat / new call
- Daily summary
Precedence rule: a user-level preference only fires if the company-level toggle for that event allows it. The company toggle is the gate; the user toggle is the opt-in within that gate.
Channels
| Channel | Implementation | Notes |
|---|---|---|
| In-app | NotificationModel + new-notification socket event | Bell + drawer in frontend/desktop; persists; deep-links via relations |
| Desktop toast | Electron native notification | Desktop app only; DesktopToast.vue for chats, LeadNotificationBanner.vue for hot leads; updates dock/taskbar count |
| Push (Android/data) | FcmService (Firebase) | High priority, channelId: 'push-notifications'; batch sends via sendEach |
| Push (iOS alert) | FcmService APNs bridge | apns-priority: 5, sound notification.wav |
| Push (iOS VoIP) | ApnService.sendVoip() | PushKit, pushType: 'voip', priority 10, silent payload — used so the device rings for incoming calls |
MailService | SMTP or SendGrid (chosen by email_client config); templates under ms-communication/app/views/templates/ | |
| Slack | SlackService | OAuth-connected; posts to a chosen channel; bidirectional for chat replies |
| Webhook | Backend webhooks | Outbound HTTP if webhooks_enabled and the event is subscribed (verify exact subscription model) |
Email rendering
Emails render server-side from EJS templates. MailService.send() wraps the body template in a layout (GENERAL.html, MINIMAL.html, WHITE_LABEL.html, or EMPTY.html):
- AI phone-call transcript emails (
inboundOutboundCallTranscript.html, sent byInboundOutboundCallController._sendTranscriptEmail) include a "Listen to Recording" button deep-linking to{portal_url}/calls?tab=transcripts. Recipients:chat_transcript_emails_to[], else the company owner; gated onsend_chat_transcript !== falseandnotifications.call !== false. Any recording URL in webhooks is a signed link (RECORDING_LINK_SECRET, served onapi.nextlevelai.site) — the email button links to the portal, not the raw audio. - Agency/internal recipients get the
MINIMAL.htmllayout. - White-label companies get
WHITE_LABEL.htmlwith the agency's name as sender, the agency'sreply_emailas reply-to, andportal_urlpointing at the white-label portal. Sender name/address swap to the white-label config. - Customizable copy is limited to the configured text fields; the layout itself is fixed (with white-label branding overrides per agency).
Every send is logged to EmailLogModel with status sent or failed (plus the error) — useful for support when a customer says "I never got the email."
Behaviors & edge cases
- Stale token cleanup (self-healing). When FCM returns
registration-token-not-registered/invalid-registration-token, the bad token is deleted fromuser_devices(_removeStaleFcmTokens). When APNs returnsBadDeviceToken/Unregistered/DeviceTokenNotForTopic, the device row is deleted (_removeStaleApnToken). So a device that uninstalled the app stops getting (failed) pushes automatically. - VoIP push is silent + high priority. Incoming-call pushes use APNs PushKit with
content-available: 1, priority 10,expiry: 0("now or never"). This lets the iOS app wake and ring even when backgrounded, but means if the device is offline at that instant the VoIP push is dropped rather than queued. - Push failures are swallowed. FCM/APNs send errors are caught and logged (to console and Loki) but do not block the rest of the notification fan-out — an in-app notification still appears even if push fails. QA: absence of a phone buzz does not mean the notification failed overall.
- First-message gating. A
NEW_MESSAGEnotification only fires on the visitor's first message in a chat (the code checkshas_previous_user_messages), so agents are not spammed on every line the visitor types. - Company-toggle gate. If a company toggle is off, the matching user-level preference never fires — this is the most common "why didn't I get notified?" cause. Support should check the company toggle first, then the user preference, then device/token registration.
- Email provider choice is global config, not per-company:
email_clientselects SMTP vs SendGrid for the whole deployment. - Daily recap dedupe.
daily_recap_email.last_sent_atprevents double-sends; if a recap is missing, check whether it was already sent that day.
Plan & limits
- Core in-app, desktop, push, and transcript/missed-call email notifications are available on all plans.
- Slack integration, scheduled report emails, and daily recap may be gated to higher tiers or require the integration to be connected — verify against the current plan matrix before quoting to a customer.
- There is no documented hard cap on notification volume; push batches are sent via
sendEach(FCM multicast), so large teams fan out efficiently.
Technical implementation
- Owning service: ms-communication.
app/helpers/NotificationHelper.js— createsNotificationModelrows, definesNOTIFICATION_TYPES.app/services/FcmService.js— Firebase push (fcmPushWithData,sendPushNotificationMsg,sendAiCallNotification,sendAll/sendEach), stale-token cleanup.app/services/ApnService.js— Apple VoIP PushKit (sendVoip), stale-token cleanup, keyknockknockAPNsAuthKey_8N644V4T9V.p8.app/services/MailService.js— SMTP/SendGrid send, EJS template rendering, white-label sender/reply-to,EmailLogModellogging.app/services/SlackService.js— OAuth, post to channel, bidirectional chat replies (handleSlackMessageForVisitor).app/services/MessagingService.js— emitsnew-notification/new-user-messageon first visitor message.
- Socket events:
new-notification(server → company room), payloadnotification_type: 'new-user-message'for chat. Seeapp/services/SocketService.js. - Storage:
notificationscollection (in-app),user_devices(FCM/APNs tokens),email_logs(delivery audit). - Desktop: desktop app renders toasts (
DesktopToast.vue), hot-lead banners (LeadNotificationBanner.vue), ringtones, and dock/taskbar badges. - Frontend: frontend renders the notification bell/drawer and Settings → Notifications.
Related
- Live Chat — transcript emails, new-message notifications, Slack bidirectional replies
- Calls — incoming-call VoIP push, missed-call and AI voice missed-call emails
- Auto-Triggers — proactive engagement that can lead to hot-lead notifications
- ms-communication service
- Nox Assistant — Nox can report which channels are enabled and why a notification did/didn't fire