Real-Time Notifications

The live bell that tells the right person the moment something needs them

What This Module Does (Plain Language)

When something happens that someone should know about — a new leave request waiting for approval, a low-stock alert, an admin announcement — POSVelo drops a notification into that person's inbox and lights up the bell icon instantly, without them refreshing the page. Each person sees only their own notifications.

Scope of this release

v2's notification system is in-app and real-time: a persisted inbox plus live delivery over WebSockets. Outbound channels (email, SMS, WhatsApp) and admin-managed templates are not part of this release — this page documents what actually ships.

How It Works End to End

1

An event happens

A business action (e.g. an employee submits a leave request) calls the notification service.

2

Fan-out to individual inboxes

The service resolves the audience — a list of users, a role, a store, or the whole tenant — into one stored row per recipient. Each row is owned by exactly one user.

3

Persist, then publish

Rows are saved to the database, then published to a Redis channel (POSVelo:notifications). Persisting first means a notification is never lost even if the user is offline — it'll be there on next load.

4

Deliver live

Every running app instance subscribes to that Redis channel and re-emits each row to its connected sockets via the notification:new event, addressed to the recipient's personal room. The user's bell updates instantly.

Why Redis is in the middle

Notifications can be raised by a background worker that has no WebSocket server of its own. Publishing through Redis decouples "who created the notification" from "who delivers it", and lets POSVelo run multiple app instances behind a load balancer — each fans out to its own clients.

Socket.IO: Rooms & the Event

On connect, an authenticated socket auto-joins a set of rooms (all tenant-prefixed):

RoomPurpose
tenant:{tenantId}:user:{userId}Personal room — where your notifications are delivered.
tenant:{tenantId}:role:{role}Role room.
tenant:{tenantId}Whole-tenant room.
tenant:{tenantId}:store:{storeId}Per-store room (one per assigned store).

The notification event itself is always delivered to the per-user room:

text

Connecting (frontend)

The browser opens a single shared Socket.IO connection, authenticated with the access token:

ts
The bell also polls — belt and suspenders

Live delivery is the fast path, but the frontend also re-fetches the unread count on a ~60-second poll and refreshes on login. So even if a socket frame is missed, the badge becomes correct within a minute. Acting on a notification immediately invalidates the cached list + unread count.

Notification Types

Every notification carries a type from this fixed set:

text

The frontend uses the type to pick an icon and to let users filter their inbox.

The Endpoints

All under /api/v2/notifications. The four inbox endpoints need no special permission — every authenticated user may read and manage their own rows (ownership is enforced server-side). Only broadcast is privileged.

MethodPathPermissionWhat it does
GET/api/v2/notificationsAny authenticatedList your notifications, newest-first, paginated. Filter by ?isRead= & ?type=.
GET/api/v2/notifications/unread-countAny authenticatedYour unread count (the bell badge).
POST/api/v2/notifications/read-allAny authenticatedMark all your unread as read; returns the count updated.
PATCH/api/v2/notifications/:id/readAny authenticatedMark one as read (404 if it isn't yours).
POST/api/v2/notifications/broadcastnotifications.send.manualSend an in-app notification to an audience.

Listing your inbox

http
json

The unread-count and mark-read responses are small and predictable:

json

Broadcasting

Pick exactly one target: a list of roles, a single storeId, or tenantWide: true. The message is persisted to every recipient's inbox and pushed live.

http
json
One target only

Supplying more than one of roles / storeId / tenantWide (or none) is a validation error. When you target a store, head-office users with no store binding (like ADMINs) are included too, so the right people still see store-floor alerts.

What Actually Triggers a Notification Today

These are the live, shipped triggers — not a wishlist:

EventTypeWho is notifiedLink
A leave request is submittedLEAVEHR_MANAGER, MANAGER, ADMIN/hr/leave/requests/:id
A leave request is approved / rejected / cancelledLEAVEThe requesting employee
Low-stock threshold crossedINVENTORYRelevant roles (store-scoped)
Admin broadcast(chosen)The chosen role / store / tenant audience(optional)
Self-clearing approval badges

A leave request notifies all approvers. The moment one of them acts on it, POSVelo resolves the now-stale "New leave request" notification for everyone else — so an approver who didn't act doesn't keep a phantom unread badge after re-login. This is done by matching the notification's data.leaveRequestId and marking those rows read.

Reliability Notes for Developers

PropertyBehavior
Fire-and-forgetDelivery never blocks the business action that triggered it — the notification service swallows its own errors. A failed publish still leaves the row in the DB.
Persist-then-publishRows are written before they're pushed, so nothing is lost if the user is offline.
Per-user ownershipReads use the tenant-scoped client AND an explicit userId filter (defence in depth). You can never read another user's row.
Multi-instance safeThe Redis bridge means every app instance delivers to its own connected clients; scaling out just works.
De-duplicated audienceFan-out de-duplicates user ids, so a user who matches by both role and store still gets exactly one row.
See it all together

For the complete endpoint catalogue (v1 + v2), socket rooms, and response envelope, head to the v2 API Reference.