Skip to main content

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:

  1. In-app record. NotificationHelper.sendNotification() writes a row to the notifications collection (NotificationModel) with company_id, optional user_id, title, message, type, and a relations object (a pointer such as { chat_id, user_session_id } or { call_id }). The relations object is what makes a notification clickable — the UI uses it to deep-link to the right screen.
  2. Real-time fan-out over socket.io. The server emits a new-notification event 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 carries notification_type: 'new-user-message'.
  3. 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.
  4. Email. Transcript emails, missed-call emails, recap/report emails are rendered from server-side EJS templates and sent via MailService (SMTP or SendGrid).
  5. 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 constantValueFires when
NEW_SESSIONnew-sessionA new visitor session starts
NEW_MESSAGEnew-messageA visitor sends their first message in a chat
NEW_CALLnew-callA new incoming call arrives
MISSED_CALLmissed-callA visitor tried to call but no agent picked up
CHAT_TRANSFERchat-transferA chat is transferred / takeover requested
AI_LEADai-leadThe 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.*)

FieldWhat it controls
chat_transcriptAfter a chat ends, email the full transcript to chat_transcript_emails_to[]
chat_transcript_emails_to[]Recipient list for transcript emails
callNotify on a new incoming call
missed_callEmail when a visitor tried to call but nobody picked up
reports_email.enabled / reports_email.frequencyPeriodic stats email — daily / weekly / monthly
daily_recap_email.enabledYesterday's summary every morning
daily_recap_email.last_sent_atTimestamp of the last recap send (used to avoid duplicates; Nox can read it)
slack_new_visitorPost to Slack on every new visitor session
slack_hot_leadPost 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

ChannelImplementationNotes
In-appNotificationModel + new-notification socket eventBell + drawer in frontend/desktop; persists; deep-links via relations
Desktop toastElectron native notificationDesktop 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 bridgeapns-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
EmailMailServiceSMTP or SendGrid (chosen by email_client config); templates under ms-communication/app/views/templates/
SlackSlackServiceOAuth-connected; posts to a chosen channel; bidirectional for chat replies
WebhookBackend webhooksOutbound 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 by InboundOutboundCallController._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 on send_chat_transcript !== false and notifications.call !== false. Any recording URL in webhooks is a signed link (RECORDING_LINK_SECRET, served on api.nextlevelai.site) — the email button links to the portal, not the raw audio.
  • Agency/internal recipients get the MINIMAL.html layout.
  • White-label companies get WHITE_LABEL.html with the agency's name as sender, the agency's reply_email as reply-to, and portal_url pointing 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 from user_devices (_removeStaleFcmTokens). When APNs returns BadDeviceToken / 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_MESSAGE notification only fires on the visitor's first message in a chat (the code checks has_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_client selects SMTP vs SendGrid for the whole deployment.
  • Daily recap dedupe. daily_recap_email.last_sent_at prevents 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 — creates NotificationModel rows, defines NOTIFICATION_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, key knockknockAPNsAuthKey_8N644V4T9V.p8.
    • app/services/MailService.js — SMTP/SendGrid send, EJS template rendering, white-label sender/reply-to, EmailLogModel logging.
    • app/services/SlackService.js — OAuth, post to channel, bidirectional chat replies (handleSlackMessageForVisitor).
    • app/services/MessagingService.js — emits new-notification / new-user-message on first visitor message.
  • Socket events: new-notification (server → company room), payload notification_type: 'new-user-message' for chat. See app/services/SocketService.js.
  • Storage: notifications collection (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.
  • 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