The notification model
When something happens in your workspace — a campaign finishes, a payment fails, a new sign-in arrives, a template is rejected — Orbit turns it into a notification and fans it out to the people who need to act on it. This page explains that model end to end: which destinations a notification can reach, how event kinds map to categories you can tune, the per-user live channel behind the bell, and how the digest scheduler turns a noisy stream into a scheduled summary. The guide at Triage the Notification Center walks through the dashboard surface; this page is the model behind it.Section 1 — The three destinations a notification can reach
Every notification Orbit emits can reach up to three destinations. Only one of them is authoritative.- The bell row (authoritative). A persistent row in the Notification Center (the page behind the bell icon at
/notifications). This row is the source of truth: it is what the unread badge counts, it is what the list page shows, and it survives until you dismiss it or it expires. Channel preferences gate outbound fan-out, but the persisted row in the Notification Center is the record every other surface reads from. - An immediate email. For event classes that aren’t security-critical or in-app-only, a per-event email fires when your category preference has email enabled and your frequency for that category is
immediate(the default). - A scheduled digest email. When you set the frequency for a category to
dailyorweekly, the per-event email is replaced by an entry in the digest queue; a background scheduler bundles pending entries into a single email at the cadence you picked.
- Security-critical classes (new device or new IP sign-in, password change, 2FA changes, API key creation or revocation, member role changes, fraud and billing anomalies) bypass per-user outbound preferences entirely. A hijacked session could otherwise mute the takeover signal before the legitimate user sees it. The dedicated security dispatch path owns their email so the generic fan-out doesn’t double-send.
- In-app-only classes (team-chat pings, webhook endpoint auto-disabled) mint the bell row but never leave the app. The bell is their only surface, so it is minted unconditionally instead of reading the outbound channel toggles.
- Always-bell classes (an email DNS health regression detected interactively on the deliverability page) mint the bell unconditionally — the daily deliverability cron writes its own bell row the same way — while their email stays gated by the normal email toggle, so the interactive page and the cron never disagree on whether the row exists.
Section 2 — Categories and the event-kind map
Each event kind (a closed enum across the platform — a campaign completion, a payment failure, a sign-in from a new device, a template approval, and the rest) maps to exactly one category. Your Settings → Notifications page shows one card per category with its own email / push / webhook toggles and its own frequency picker. The categories, with examples of the kinds that land in each:
A kind that has no explicit mapping falls back to
system — the broadest default-on bucket — so a forgotten mapping never silently routes an event into a category you disabled. The category assignment is what makes the Settings → Notifications toggles meaningful: unticking “Messaging → Email” suppresses exactly the kinds listed under that row and nothing else. The same category vocabulary carries into the digest scheduler (below).
Section 3 — The per-user live channel
The bell’s live view rides a server-sent events channel scoped to a single user (notifications:user:<userId> on the wire). When a notification row is minted for you, the fan-out publishes the row onto your own channel, so the badge and dropdown update without polling and without broadcasting your teammates’ rows to every open tab in the org.
If you are offline when the event fires, nothing is lost: the row persists and the badge catches up on the next list fetch. A Redis hiccup during the publish is degradable the same way — the row is already written, the push is best-effort, and the initial GET on your next sign-in reconciles. Channel scoping is per-user, not per-org, because one tenant with hundreds of operators should not page every browser on every event.
Section 4 — The digest model
The digest is an org-level decision with per-recipient opt-in. Four pieces cooperate:- Org master toggle, frequency, and category filter. The org holds one settings row: on/off, a default frequency (
dailyorweekly), and a category filter (empty filter = all categories). Until an admin enables the digest for the org, per-user daily/weekly choices queue rows but send nothing — flipping the toggle drains the backlog on the next tick. - Per-(org, user) recipient registry. Each member has an
opted_inflag. Until anyone touches it, the UI synthesizes a default registry in which admins are opted in; the first explicit toggle writes the real row. Opted-in recipients are the ones a digest email is actually addressed to. - The per-user frequency field. On Settings → Notifications each user sets a per-category frequency.
immediate(the default) fires the per-event email path;dailyorweeklyenqueues the event into the digest queue instead. The bell row is written in all cases — frequency only gates the outbound email. - The scheduler. On a ten-minute cadence it sweeps the due rows of the digest queue (locked reads so concurrently running workers can’t double-send), groups them per user, and sends one email grouping every pending entry for the window. A successful send stamps the rows sent; a failure leaves them for the next tick. An org with nothing pending — or a digest composition that comes back empty — is skipped rather than emailed an empty summary. The window-to-window overlap guard uses the org’s last-digest timestamp.
POST /api/v1/notifications/digest/preview, optionally with a bounded lookback override) — the call Settings → Notifications → Digest makes when it renders its live preview card. Composition reads the tenant’s own ledger, inbox, and notification tables, so the preview reflects real data.
Section 5 — Billing and channel-quality alerts in the same model
Low-balance and channel-quality events are notifications before they are anything else, and they reuse the same pipeline:- Billing.
payment_failed,top_up_succeeded,credit_low,wallet_emptied,number_renewal_failed, andunusual_billing_activityall map to thebillingcategory, so one toggle shapes all of them. The pipeline that debits your wallet is described in How billing meters your usage; the kinds above are the alert surface on top of that pipeline. - Channel quality. WhatsApp template approvals or rejections (
whatsapp_template_status_changed), sender-ID review outcomes, port-in status changes, and deliverability alert rule firings route throughmessaging; SLA or KPI rule firings route throughagents. Filtering by category on the Notification Center page separates “money events” from “channel events” without remembering individual kind names.
A worked example: tracing whatsapp template auto-paused
Suppose Meta scores your holiday_promo template RED and the template has auto-pause enabled. Orbit pauses it before Meta penalizes the account, and the event flows end to end:
- Event minted. The event kind is
whatsapp_template_status_changed-class WhatsApp quality signal; it maps to themessagingcategory with a per-event severity. - Recipients resolved. The fan-out looks up the org’s admin and owner set (the people who manage messaging channels).
- Bell row minted. A row lands in the Notification Center titled “WhatsApp template paused” with the template name and an action link to the templates page. This row exists regardless of outbound choices.
- Live badge. Your per-user channel pushes the row, so the bell badge increments while the dashboard is open.
- Outbound gated by your preference. If your
messaging → emailtoggle is on with frequencyimmediate, a per-event email goes out now. If you setmessagingfrequency todaily, the event enters the digest queue instead and arrives in tomorrow’s single digest email. If the org digest toggle is off, that queue entry sits unsent. - Digest composition. When the scheduler ticks with the org enabled and you opted in, the event appears as a highlight (title, summary, action link) inside the one email you get for that window — together with every other
messagingentry that accumulated.
Where this fits
- Triage the Notification Center — the dashboard surface this model backs: read/unread, dismiss, filter chips.
- Webhook fan-out and event sinks — the developer-facing event family. Notifications are operator-facing; webhooks and sinks are the separate system your integrations subscribe to.
- How billing meters your usage — the rating pipeline the billing notification classes sit on top of.