Documentation

Notifications

Move from toasts to notifications

Upgrade a VitNode install to stored notifications and replace a plugin's realtime toasts with c.get("notifications").publish(), so members see them after a reload or when they come back.

Before stored notifications, a VitNode "notification" was a toast pushed over the WebSocket. It reached open tabs only and was gone after a reload. This guide upgrades your database and turns the toasts members would look for later into stored notifications. Looking for a fix instead? See Troubleshooting.

Apply the database migration

Stored notifications add six core_notification* tables. Run the migrations once after upgrading @vitnode/core:

Run migrations
bun run db:migrate

In development, pnpm dev runs vitnode db:prepare, which applies them for you.

Pick the toasts to keep

A reply, a mention, an approval or a finished export belongs in the inbox, because a member may look for it later. Feedback about something happening right now, such as "Import started", can stay a toast.

Declare a type for each

Declare and register one type per kept toast, as in Publish notifications:

plugins/shop/src/api/lib/notifications.ts
import { buildNotificationType } from '@vitnode/core/api/lib/notifications/registry'
import { z } from 'zod'

export const exportReadyNotification = buildNotificationType({
  id: 'shop.export_ready',
  version: 1,
  schema: z.object({ exportId: z.number() }),
  category: 'system',
  label: '@acme/shop.notifications.export_ready.label',
  defaults: { inApp: true, email: 'none' },
  present: ({ data, t }) => ({
    title: t('@acme/shop.notifications.export_ready.title'),
    target: `/shop/exports/${data.exportId}`,
  }),
})

Replace the toast with publish()

plugins/shop/src/api/modules/exports/routes/finish.route.ts
c.get('realtime').sendToUser(user.id, notificationsChannel, {
  title: 'Your export is ready',
  type: 'success',
})
await c.get('notifications').publish({
  type: exportReadyNotification,
  recipients: [user.id],
  allowSelf: true,
  data: { exportId: job.id },
  idempotencyKey: `export:${job.id}`,
})

The bell updates over the same WebSocket, so members still see it right away. allowSelf: true matters here: the member who started the export is also the request's actor, and the actor is skipped by default.

Check the result

  1. Run the export as a member, then reload the page. The notification is still under the bell, with an unread count.
  2. Sign out and back in on another browser. The same notification is there.
  3. On /settings/notifications, the new type is listed and can be turned off.

The AdminCP dashboard's Send a notification widget already works this way: it shows a toast and stores the message as a core.admin_message notification. Toasts sent with sendToUser() and notificationsChannel keep working unchanged, see WebSocket.