Documentation

Notifications

Inbox and realtime

How the VitNode notification inbox tracks read state, groups related notifications, keeps the unread count exact and pushes it live over the WebSocket.

Every recipient gets their own inbox items, and every member has one stored unread count. Each change to an inbox updates that count in the same database transaction and then pushes the new value to the member's open tabs. This page explains how read state, grouping and the live count behave, so you can predict what members see.

Read and unread

An inbox item is unread while it is not archived and the member has not read its latest activity. Each item has two counters:

  • activitySeq starts at 1 and increases each time a grouped notification joins the item.
  • readSeq records how far the member has read.

Opening /notifications or the bell never marks anything read. Opening an item, marking it read or Mark all as read does.

When the browser marks an item read, it sends the activitySeq it displayed as throughSeq. If a fourth reply joined the item after the bell rendered it, the item stays unread, because the member has not seen that reply yet.

Mark all as read marks everything that exists when the request takes the member's lock. A notification delivered a moment later stays unread, so nothing that arrives during the click is lost.

Grouping

Types with grouping merge related notifications into one item per member, type, group key and time window:

  • Windows are fixed UTC slots, not sliding ones. With a 60-minute window, 14:00 to 14:59 UTC share an item, and 15:00 starts a new one.
  • A new notification in a read item makes it unread again, and one in an archived item brings it back to the inbox.
  • Items sort by their latest activity, so a grouped item moves to the top when it grows.
  • The unread count counts items, not notifications. Ten replies in one grouped item add 1.

Items that are no longer available

Before returning a page of the inbox, core checks every item again: the type is still registered, its stored data still parses, access still allows the member and present still renders. If any check fails, the item comes back as a placeholder with the title "This notification is no longer available." and no link.

The placeholder keeps its read state, so members can still read or archive it, and the unread count never includes an item they cannot clear. This covers deleted content, revoked access and uninstalled plugins.

The live unread count

The browser receives the same { unread, revision } pair from three places:

  1. The session payload, user.notifications in GET /api/@vitnode/core/users/session, read fresh on every request.
  2. The WebSocket message notificationsStateChannel, sent after every committed inbox change and once each time a signed-in socket opens.
  3. GET /api/@vitnode/core/notifications/state, only used when there is no socket.

unread is always the absolute count. revision increases with every change, and the browser keeps the state with the highest revision, so a slow response never undoes a newer realtime update. Messages are sent only after commit, so a rolled-back change never reaches the browser.

WebSocket messages missed while a tab was offline are not replayed. Instead, the server sends the current count as the first message on every new connection, so a reconnect catches up without an extra request. While the socket is open, the browser never calls /state. Without a socket, for example when the API runs inside the web app on the same origin or the connection is down, it reads /state every 60 seconds while the tab is visible, and right away when a tab returns after more than 30 seconds in the background.

When the count changes, open notification lists refresh once per burst: 1 second after the last change and at most 5 seconds after the first.

Several API instances need Redis for realtime

With REDIS_URL set, realtime messages travel between API instances over Redis, so delivery on instance A reaches a tab connected to instance B. Without Redis, only tabs connected to the same instance update live. Others catch up on their next reconnect or page load. See Redis.

What core ships in the UI

PieceWhere
Bell with unread badgeSite header on every screen size. Loads the recent list only when opened
Notifications page/notifications, with All and Unread tabs, a category filter and infinite scroll
Preferences/settings/notifications, see Preferences

A default theme needs no wiring. To build your own bell, call the member endpoints and listen to the realtime channel.