The notifications engine is a persistent, multi-transport, RBAC-aware subsystem that delivers alerts to (1) the Tauri desktop app as a native OS toast plus an in-app webview banner and (2) LAN browser sessions over a Server-Sent Events (SSE) stream. It covers monitor alarm pushes — the only wired producer today — as well as system events (typed via a Source enum) and admin broadcasts. Storage reuses the SQLite meta_data BLOB plus denormalized-columns pattern of the sqlite_api layer (clarity:backend/src-tauri/src/notifications/mod.rs:1-15).
Defined in model.rs.
Notification struct (clarity:backend/src-tauri/src/notifications/model.rs:118-159). Denormalized SQL columns: id, user_id (matches users.email; None = broadcast to all recipients), orgs_id, sites_id, units_id. Other fields: severity, source, title, body, link, redirect (bool — click-to-navigate), payload (JSON), source_ref, created_at, expires_at, read_at (None = unread), delivered_at (None = never delivered live).Severity enum: Info | Warn | Error | Critical, serialized serde-lowercase (clarity:backend/src-tauri/src/notifications/model.rs:21-55).Source enum: Monitor | ProcessManager | Licensing | Backup | System | Admin | Custom, serialized snake_case (clarity:backend/src-tauri/src/notifications/model.rs:58-101).clarity:backend/src-tauri/src/notifications/model.rs:9-14). Enforced by NotificationCreate::validate(), which also rejects redirect=true without a link, and any expires_at in the past (clarity:backend/src-tauri/src/notifications/model.rs:237-284).NotificationCreate — producer input; includes an optional NotificationEmail (clarity:backend/src-tauri/src/notifications/model.rs:169-231).NotificationEvent — typed producer events: MonitorAlarmOpened, MonitorAlarmClosed, Custom(NotificationCreate) (clarity:backend/src-tauri/src/notifications/model.rs:318-346). Phase 1 is monitor-only.NotificationMeta — the BLOB subset persisted to meta_data; redirect is #[serde(default)] for back-compat with rows written before that field existed (clarity:backend/src-tauri/src/notifications/model.rs:435-473).Custom routes are composed in the filter chain at clarity:backend/src-tauri/src/notifications/routes.rs:60-124. Standard list / get / count / findOne / bulk / update / delete are not defined here — they come from the generic CRUD macro registered in the sqlite_api layer (sqlite_api/api/warp_routes.rs), as noted at clarity:backend/src-tauri/src/notifications/routes.rs:1-13.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST |
/exactapi/notifications |
Admin only (see note) | Validated create; delegates to NotificationService::send(). Filter clarity:backend/src-tauri/src/notifications/routes.rs:76-87, handler clarity:backend/src-tauri/src/notifications/routes.rs:257-286. |
GET |
/exactapi/notifications/stream |
Header or ?access_token= query param |
SSE stream. Filter clarity:backend/src-tauri/src/notifications/routes.rs:93-101. |
POST |
/exactapi/notifications/{id}/read |
Any logged-in user | Mark read. Handler clarity:backend/src-tauri/src/notifications/routes.rs:304-323. |
GET |
/exactapi/notifications/unread-count |
Any logged-in user | Bell-badge count. Handler clarity:backend/src-tauri/src/notifications/routes.rs:295-302. |
NOTE — admin enforcement on create: the create filter uses
with_auth(), which admits any logged-in user. The admin restriction is actually the in-handler checkclaims.role != "admin"atclarity:backend/src-tauri/src/notifications/routes.rs:264, not the filter. The doc comment in the code implyingwith_auth()enforces admin is slightly misleading — the effective gate is in the handler body.
The stream endpoint GET /exactapi/notifications/stream is served by the handler at clarity:backend/src-tauri/src/notifications/sse.rs:60-87. The flow:
Wire format (documented at clarity:backend/src-tauri/src/notifications/sse.rs:6-40): event: notification frames each carrying an id:, then a replay-complete event carrying {replayed:n}, followed by live events, plus a : keepalive comment every 25 s. The stream is built with async_stream::stream! wrapped by Body::wrap_stream (clarity:backend/src-tauri/src/notifications/sse.rs:97-250).
Deduplication uses a last_yielded_ts watermark. Per-recipient visibility is filtered on receive via convert::visible_to. On a broadcast Lagged error it re-fetches undelivered rows since the watermark. Response headers are set to Content-Type: text/event-stream and X-Accel-Buffering: no (to disable nginx buffering).
The Transport trait is defined at clarity:backend/src-tauri/src/notifications/transports.rs:71-78, with a DeliveryOutcome enum — Delivered{count}, NoReceivers, Transient, Permanent — at clarity:backend/src-tauri/src/notifications/transports.rs:46-65.
Only one implementation ships today, LocalTransport (clarity:backend/src-tauri/src/notifications/transports.rs:87-319), which fans out to:
webview.eval() injecting toast.js;tauri-plugin-notification;broadcast::Sender that feeds the SSE subscribers.Email, WebSocket, and WebPush transports are explicitly marked future work (clarity:backend/src-tauri/src/notifications/transports.rs:13-15).
NOTE — email is supported, but not as a
Transport. It is handled by the dispatcher calling the separate mail module (see below).
Producers call NotificationService::try_send_event(event) — non-blocking and lossy over a bounded mpsc of 1024 (clarity:backend/src-tauri/src/notifications/service.rs:270-295).
Dispatcher drains the mpsc in run_dispatcher (clarity:backend/src-tauri/src/notifications/dispatcher.rs:21-32). event_to_create maps MonitorAlarmOpened → Warn/Error by priority and MonitorAlarmClosed → Info, both with link=/alarms/{id}, redirect=true, and source=Monitor (clarity:backend/src-tauri/src/notifications/dispatcher.rs:133-224). If a NotificationCreate.email is set, after persisting the notification the dispatcher builds a mail::model::MailSendRequest and either sends immediately (then enqueues and marks sent) or enqueues for retry (clarity:backend/src-tauri/src/notifications/dispatcher.rs:48-125). This is the bridge into the mail module.
Service send() validates the input, stamps server-managed fields, persists via the CRUD insert_item, then fans out through all transports and marks deliveredAt asynchronously if any transport reported delivery (clarity:backend/src-tauri/src/notifications/service.rs:181-264). A global OnceLock holds the service; init() spawns the dispatcher and a retention sweep (clarity:backend/src-tauri/src/notifications/service.rs:85-165).
Store (store.rs) holds the custom SQL beyond CRUD:
un_delivered_visible_to_since — SSE replay query, LIMIT 100, non-expired rows only (clarity:backend/src-tauri/src/notifications/store.rs:134-201).unread_count_visible — bell-badge count (clarity:backend/src-tauri/src/notifications/store.rs:209-234).mark_read_visible — atomic, visibility-guarded, uses SQLite json_set (clarity:backend/src-tauri/src/notifications/store.rs:243-301).mark_delivered (clarity:backend/src-tauri/src/notifications/store.rs:309-326).purge_older_than — retention sweep; deletes read rows only (clarity:backend/src-tauri/src/notifications/store.rs:330-344).clarity:backend/src-tauri/src/notifications/store.rs:63-118): admin bypasses scoping; a non-admin with no scope match resolves to 1=0.Convert (convert.rs) translates between the typed in-process Notification and the CRUD/storage row model (meta_data: JsonBlob): from_crud (clarity:backend/src-tauri/src/notifications/convert.rs:48-69), to_crud/to_crud_json (clarity:backend/src-tauri/src/notifications/convert.rs:76-129), and the in-memory visible_to RBAC predicate (clarity:backend/src-tauri/src/notifications/convert.rs:136-166).
The service is initialized via notifications::init_service(db, app_handle) after the DB is up and before the monitor agent (clarity:backend/src-tauri/src/main.rs:5269-5274). Route registration is placed first in the warp filter composition so the custom routes win over the generic CRUD POST /notifications (clarity:backend/src-tauri/src/main.rs:5456-5457). The SSE stream is layered un-wrapped, because warp::log cannot wrap its non-'static streamed reply. See API Server for the overall filter composition.
The only wired producer is the monitor agent, which calls try_send_event on alarm transitions:
MonitorAlarmOpened at clarity:backend/src-tauri/src/monitor/agent.rs:213-229 — priority "danger" when the rule comparison is GreaterThan, otherwise "warning".MonitorAlarmClosed at clarity:backend/src-tauri/src/monitor/agent.rs:238-249.For example, a rule on idfan_vibration for unit boiler-2 (organization acme-power, site plant-1, grid default_grid) crossing its GreaterThan threshold produces a danger-priority MonitorAlarmOpened event, which the dispatcher maps to an Error-severity notification linking to /alarms/{id}.
KNOWN GAP: monitor notifications set only
units_id;orgs_idandsites_idare alwaysNone. Their RBAC visibility therefore relies on unit scope or the admin role.
Retention is governed by two settings (see API Server for config loading detail):
clarity.notifications.retention_days — default 30; 0 disables the purge (unread rows are always kept regardless).clarity.notifications.retention_interval_seconds — default 86400.Both are read at clarity:backend/src-tauri/src/config.rs:449-455.
Notification client libraries for Python and TypeScript are documented at Notification SDKs.
Last updated: 2026-07-17 from commit 6800acc