Notifications
Per-type gating, a link-keyed data model, and read state that follows naturally from viewing the thing it's about.
Triggers#
MENTION, REVIEW_REQUESTED, PR_COMMENT, APPROVAL, PIPELINE_FAILED, ISSUE_ASSIGNED, REPOSITORY_ACTIVITY, and CHAT_MESSAGE are all individually gateable per user under Settings → Notifications — disabling a type suppresses creation of that notification entirely (server-side, at fan-out time), rather than merely hiding it client-side.
Data model#
A Notification row is { userId, type, title, body?, link?, senderId?, readAt?, createdAt }. link is what ties a notification back to the resource it's about (e.g. /chat?channel=<id> for a chat message) and is the join key used for the read-state propagation described below — it is a plain URL, not a foreign key, which is what lets a single notification-clearing mechanism work uniformly across chat, PRs, issues, and anything else that has a canonical link.
Read-state propagation#
Rather than requiring every notification to be dismissed one at a time, read state is designed to follow naturally from viewing the thing the notification is about:
- Opening the Notifications page marks everything currently listed as read shortly after it renders, without altering which rows show the "unread" highlight for that view — so the bell badge clears immediately while you can still see, at a glance, what was new in this visit.
- Any notification whose link points at a chat channel is marked read the instant that channel is opened by any route (bell, deep link, or normal navigation into Chat) — see Chat.
- Clicking an individual notification (from the Notifications page or a dashboard preview) marks that one read as part of navigating to its link.
Delivery#
Notifications are not pushed over the WebSocket gateway — the bell badge and the Chat sidebar's unread count are both short-interval polled REST queries (GET /api/notifications/unread-count, GET /api/chat/unread-count). This is a deliberate simplicity trade-off: it bounds notification staleness to the poll interval without requiring every client to hold a live socket subscription to a second event stream on top of the chat gateway.
