Documentation

Notifications

Notifications API reference

Reference for c.get("notifications") in VitNode (publish, remove and state), its errors, the member and AdminCP REST endpoints, and the realtime channel.

c.get("notifications") is available in every Hono route, event listener and queue task. This page lists its methods, the REST endpoints behind the bell and the AdminCP screen, and the WebSocket channel that carries the unread count. For the type definition, see Notification types.

Methods

MethodReturnsBehavior
publish(args)Promise<{ eventId: number; duplicate: boolean }>Stores one event and queues its delivery. Does not wait for delivery
remove(args)Promise<number>Deletes matching inbox items and lowers unread counts. Returns the count
state(userId)Promise<{ unread: number; revision: number }>Reads one member's stored unread count

publish

Prop

Type

Publishing the same type and idempotencyKey again returns the first event with duplicate: true.

remove

Prop

Type

Without type and userIds, remove() deletes every item about the subject for everyone.

state

plugins/forum/src/api/modules/topics/routes/summary.route.ts
const { unread, revision } = await c.get('notifications').state(user.id)

revision increases with every change to the member's inbox, so a client can tell a newer count from an older one.

Errors

publish() throws a NotificationPublishError before writing anything when:

  • the type is not registered, or type is a different object than the registered one;
  • data fails the schema. The message lists each failing path;
  • idempotencyKey is empty or longer than 255 characters after trimming;
  • subject has an invalid type or id, or a type other than the type's subjectType;
  • there are more than 100,000 recipients.

It also throws when two transactions publish the same key at the same moment and the first has not committed. Retry after the first commits, and the call returns duplicate: true.

Member endpoints

Every route acts on the signed-in member and answers 401 to guests. Paths are relative to /api/@vitnode/core/notifications.

MethodPathBehavior
GET/One page of items, newest activity first. Query: limit (1 to 50, default 20), cursor, unread, category, type
GET/state{ unread, revision }
POST/{id}/readMarks one item read. Optional body { throughSeq }
POST/{id}/unreadMarks one item unread again
POST/{id}/archiveHides one item from the inbox. New activity brings it back
POST/read-allMarks everything read. Optional body { category }
GET/preferencesEvery type the member can see, with their channels
PUT/preferencesSaves { types: { [typeId]: { inApp?, push?, email? } } }. Other types keep their choices

The mark routes return the new { unread, revision }. Another member's item id answers 404, like a missing one. Call these routes with the fetcher:

const state = await fetcher({
  plugin: '@vitnode/core',
  method: 'get',
  module: 'notifications',
  path: '/state',
})

A member's time zone, used for digests, is saved with PUT /api/@vitnode/core/users/me/time-zone and the body { "timeZone": "Europe/Warsaw" }, or null to follow their language.

AdminCP endpoints

Paths are relative to /api/@vitnode/core/admin/notifications and need an AdminCP session with the listed staff permission in the notifications module.

MethodPathPermissionBehavior
GET/overviewcan_viewRegistered types, installation settings and delivery health
GET/statscan_viewActivity numbers. Query: range (24h, 7d, 30d), timeZone
GET/deliveriescan_viewEmail deliveries, newest first. Query: status, limit, cursor
PUT/types/{type}can_editMerges the body into the type's policy
POST/test-emailcan_manageQueues a test email to the signed-in administrator only
POST/reconcilecan_manageCompares unread counts with the inbox. Body { dryRun, userId? }
POST/deliveries/{id}/retrycan_manageRetries one failed delivery with the same idempotency key
POST/pausecan_managePauses delivery and email
POST/resumecan_manageResumes and re-queues events that waited
POST/emails/cancelcan_manageSkips every email that has not gone out
POST/read-allcan_manageMarks every member's notifications read
POST/delete-allcan_manageDeletes every member's notifications
POST/members/reset-preferencescan_manageClears every member's own choices

The body of PUT /types/{type} accepts any of these keys:

{
  "allowInApp": true,
  "inApp": false,
  "allowPush": true,
  "allowEmail": true,
  "email": "daily",
  "memberCanEdit": false
}

POST /reconcile only reports by default: dryRun is true unless you send false. It returns { mismatched, corrected }.

Realtime channel

Every committed change to a member's inbox sends notificationsStateChannel from @vitnode/core/ws/notifications to that member's open tabs. A signed-in socket also gets one message with reason: 'reconciled' right after it connects:

interface NotificationStateMessage {
  unread: number
  revision: number
  reason:
    | 'created'
    | 'read'
    | 'unread'
    | 'read_all'
    | 'archived'
    | 'removed'
    | 'reconciled'
  notificationId?: number
}

unread is always the absolute count, never a change of one. The toast channel, notificationsChannel, lives in the same module. See WebSocket.

AdminCP actions also emit events such as notifications.paused and notifications.type.updated. See Built-in events.