Notifications
Add persistent notifications to a VitNode plugin - an inbox, a live unread count, push and email choices, daily or weekly digests, all from one publish call.
The Notifications Center is VitNode's inbox. A plugin publishes an event with
c.get("notifications").publish(), and core stores it, works out who receives
it, updates every unread count in realtime and sends the emails and digests
users asked for.
Your plugin decides who might care. Core decides who actually receives what, and delivers it.
Quick start
Declare a notification type
import { buildNotificationType } from '@vitnode/core/api/lib/notifications/registry'
import { z } from 'zod'
export const topicReplyNotification = buildNotificationType({
id: 'forum.topic_reply',
version: 1,
schema: z.object({ topicId: z.number(), topicTitle: z.string() }),
category: 'social',
label: '@acme/forum.notifications.topic_reply.label',
defaults: { inApp: true, email: 'daily' },
email: true,
present: ({ data, t }) => ({
title: t('@acme/forum.notifications.topic_reply.title', {
title: data.topicTitle,
}),
target: `/forum/topics/${data.topicId}`,
}),
})Every field is described in Notification types.
Register it on your API plugin
import { buildApiPlugin } from '@vitnode/core/api/lib/plugin'
import { topicReplyNotification } from '@/api/lib/notifications'
import { CONFIG_PLUGIN } from '@/const'
import apiMessages from '@/locales/api'
export const forumApiPlugin = () =>
buildApiPlugin({
pluginId: CONFIG_PLUGIN.pluginId,
messages: apiMessages,
notificationTypes: [topicReplyNotification],
modules: [topicsModule],
})The label and title keys live in the plugin's API messages
(src/locales/api/en.json), because the server renders notifications. See
Server-side translations.
Publish when something happens
await c.get('db').transaction(async (tx) => {
const [reply] = await tx.insert(forum_replies).values(values).returning()
await c.get('notifications').publish({
type: topicReplyNotification,
tx,
recipients: [topic.authorId],
subject: { type: 'forum.topic', id: topic.id },
data: { topicId: topic.id, topicTitle: topic.title },
idempotencyKey: `reply:${reply.id}`,
})
})The topic author sees it in the bell a moment after the response is sent. See Publish notifications.
Apply the database migration
The Notifications Center adds six core_notification* tables. Build and
migrate once after upgrading:
bun run build:plugins && bun run db:migrateWho does what
| Your plugin | Core |
|---|---|
| Declares notification types | Builds the preferences page from them |
Picks candidates: recipients | Removes the actor, deleted users and opted-out users |
Answers "who may see this?" in access | Calls it in batches, again before email, and when listing |
Renders plain text in present | Strips markup, drops unsafe links, localizes per recipient |
Publishes once per real-world happening (idempotencyKey) | Deduplicates, retries and never delivers twice in-app |
| Unread counts, realtime, grouping, email, digests and cleanup |
How a notification travels
publish() one row in core_notification_events
│ + a "notifications-fanout" queue task
│ (same transaction when you pass tx)
▼
fan-out, in batches of 500 candidates → opted out? access?
│
├─► receipt per event and user the "already delivered" guard
├─► inbox item (or grouped) core_notifications
├─► unread count + revision core_notification_user_state
│ └─► WebSocket push notificationsStateChannel
└─► email delivery immediate now, digests on schedule
└─► "notifications-email" queue task → your email adapter- Stored once. An event is one row however many people receive it.
- Durable fan-out. A queue task delivers it in batches. Each batch commits with its cursor, so a crash resumes where it stopped.
- Per-recipient inbox. Every recipient gets an inbox item. Types with
groupingmerge related events into one item. - Counter and realtime. Each user has one stored unread count with a revision. Every change pushes the new absolute count over the WebSocket.
- Email and digests. Email runs in its own queue task, so a slow mail provider only delays email - never the inbox.
Learn more
Notification types
Every field of buildNotificationType
Publish notifications
Transactions, idempotency keys, audiences and access checks
Inbox and realtime
Read state, grouping, the unread count and the WebSocket
Preferences
How core decides what each user receives
Email and digests
Immediate email, daily and weekly digests, retries
Custom Event Adapter
Build a custom event transport adapter to distribute VitNode domain events across multiple instances via message brokers like Redis Streams or RabbitMQ.
Notification types
Reference for buildNotificationType - ids, schema versions, presentation, access checks, grouping, email and mandatory types.