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.
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
An event happens
A business action (e.g. an employee submits a leave request) calls the notification service.
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.
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.
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.
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):
| Room | Purpose |
|---|---|
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:
Connecting (frontend)
The browser opens a single shared Socket.IO connection, authenticated with the access token:
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:
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.
| Method | Path | Permission | What it does |
|---|---|---|---|
| GET | /api/v2/notifications | Any authenticated | List your notifications, newest-first, paginated. Filter by ?isRead= & ?type=. |
| GET | /api/v2/notifications/unread-count | Any authenticated | Your unread count (the bell badge). |
| POST | /api/v2/notifications/read-all | Any authenticated | Mark all your unread as read; returns the count updated. |
| PATCH | /api/v2/notifications/:id/read | Any authenticated | Mark one as read (404 if it isn't yours). |
| POST | /api/v2/notifications/broadcast | notifications.send.manual | Send an in-app notification to an audience. |
Listing your inbox
The unread-count and mark-read responses are small and predictable:
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.
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:
| Event | Type | Who is notified | Link |
|---|---|---|---|
| A leave request is submitted | LEAVE | HR_MANAGER, MANAGER, ADMIN | /hr/leave/requests/:id |
| A leave request is approved / rejected / cancelled | LEAVE | The requesting employee | — |
| Low-stock threshold crossed | INVENTORY | Relevant roles (store-scoped) | — |
| Admin broadcast | (chosen) | The chosen role / store / tenant audience | (optional) |
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
| Property | Behavior |
|---|---|
| Fire-and-forget | Delivery 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-publish | Rows are written before they're pushed, so nothing is lost if the user is offline. |
| Per-user ownership | Reads use the tenant-scoped client AND an explicit userId filter (defence in depth). You can never read another user's row. |
| Multi-instance safe | The Redis bridge means every app instance delivers to its own connected clients; scaling out just works. |
| De-duplicated audience | Fan-out de-duplicates user ids, so a user who matches by both role and store still gets exactly one row. |
For the complete endpoint catalogue (v1 + v2), socket rooms, and response envelope, head to the v2 API Reference.