The bell stops being a stub. Unread count, an inbox with per-item read/archive, “mark all read”, and a live SSE stream that prepends arrivals without a refetch.
Read and unread are visually distinct without relying on color alone — the unread rows also carry the leading dot and a tinted surface.
Degrades quiet: when the service is unreachable the bell shows no badge and the panel shows the error — a notification outage must never break the shell.
Left to right: unread, none, capped at 99+, and service
unreachable. The degraded bell is indistinguishable from “no unread” on purpose — a phantom
badge the user can never clear is worse than a missing one.
Arrivals prepend and bump the badge. On reconnect the client re-fetches the unread count, so an event missed during a drop self-heals rather than under-counting forever.
| Method | Path | Notes |
|---|---|---|
GET | /api/v1/notifications | Paginated inbox; status, category, organizationId filters. |
GET | /api/v1/notifications/unread-count | The badge. |
GET | /api/v1/notifications/stream | SSE, heartbeat-kept. |
POST | /api/v1/notifications/{id}/read | Also /read-all, /{id}/unread, /seen. |
DELETE | /api/v1/notifications/{id} | Archive. |
GET/PUT | /api/v1/notifications/preferences | Per category × channel. |
POST | /api/v1/notifications/internal/publish | Service-to-service only; internal token; fans out to recipients. |
user_id from the verified JWT and never from
the request, so there is no id to tamper into someone else's mailbox.dedupe_key, unique per user among live rows, means a retried producer cannot
double-post the same event.[data-panel='notifications'] ·
[data-state='loading'|'empty'|'error'|'ready'] · [data-bell] ·
[data-badge] · [data-notification] · [data-read] ·
[data-action='mark-all-read'|'archive'|'retry'] · [data-arrival='live'].