Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
added undocumented endpoints (ton-proof challenge, profile email verify,
GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
90-day notification TTL, soft-delete semantics, wallet fields
Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation
Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 KiB
title, tags, related_models, related_apis
| title | tags | related_models | related_apis | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Notification Flow |
|
|
|
Last updated: 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
Notification Flow
Cross-cutting flow that powers the bell icon, toast pop-ups, badge counters, and (optionally) email digests. Notifications are created by many services and travel to the user via both MongoDB persistence and Socket.IO push.
Trigger sources
- Purchase request lifecycle — created, status changed, cancelled (
PurchaseRequestService). - Offer lifecycle — new offer, accepted, rejected, withdrawn (
SellerOfferService). - Payment lifecycle — confirmed, refunded, payout sent (
shkeeperWebhook,PurchaseRequestService.notifyPaymentConfirmed). - Chat —
chat-notificationevents (see Chat Flow). - Dispute — created, assigned, resolved (TODO in
DisputeService; the chat itself notifies). - Delivery — code generated, delivery confirmed (
DeliveryService). - Referral — sign-up via referral code (
AuthController.verifyEmailWithCode,googleSignUp). - Points / levels —
level-upevent when crossing a tier (PointsService.addPoints:91-99). - Admin actions — e.g. dispute resolution, manual payouts.
Actors
- System — the various services calling
NotificationService.createNotification. - User — the recipient.
- Frontend — bell-icon dropdown and toast subscribers in
frontend/src/layouts/components/notifications-drawer/and the global socket provider. - Backend —
NotificationService(backend/src/services/notification/NotificationService.ts), routes at/api/notifications. - MongoDB —
notificationscollection (one document per notification). Notifications are auto-deleted after 90 days (TTL index oncreatedAt). - Socket.IO — emits
new-notificationtouser-{userId}. - Email (optional) — periodic digest worker (not implemented today; planned).
Step-by-step narrative
Creating a notification
- Any service builds a
NotificationCreateDataobject:{ userId, title, message, type: 'info' | 'success' | 'warning' | 'error', category: 'purchase_request' | 'offer' | 'payment' | 'delivery' | 'system', relatedId?, metadata?, actionUrl? } - Calls
await notificationService.createNotification(data). NotificationService.createNotification(NotificationService.ts:18-37):Notification.create({ ...data, isRead: false, createdAt: now }).- Calls
this.emitRealTimeNotification(userId, saved)whichglobal.io.to('user-${userId}').emit('new-notification', payload).
- The notification is persisted and pushed simultaneously.
Frontend reception
- The frontend's global Socket.IO provider listens for
new-notificationevents on itsuser-{me}room (joined on app mount viasocket.emit('join-user-room', userId)). - On event: increment the bell-icon badge, optionally show a toast (
notistack), and prepend the entry into the cached notifications list (React Query cache). - The bell-icon dropdown also fetches
GET /api/notifications?page=1&limit=20for paginated history.
Reading
- User opens the bell-icon dropdown — frontend calls
PATCH /api/notifications/:id/readfor each viewed entry, orPATCH /api/notifications/mark-all-readto clear all at once. NotificationService.markAsRead(notificationId, userId)(NotificationService.ts:74-90):Notification.findOneAndUpdate({ _id, userId }, { isRead: true, readAt: now }).- After updating, the backend emits
unread-count-updatetouser-{userId}so all open tabs (and other devices) immediately sync their badge counter.
Purchase request status coverage gap
NotificationService.notifyRequestStatusChanged handles many purchase-request statuses but does not emit notifications for pending_payment or seller_paid. If a buyer moves to pending_payment or a seller is marked seller_paid, no notification is created. This is a known coverage gap; add dedicated helper methods (or extend the switch-case) if those transitions need to surface to recipients.
Preferences
User.preferences.notifications(in the User schema) can hold per-category opt-outs (emailNotifications,pushNotifications, etc.). The current implementation does not enforce preferences at send-time — all enabled notifications fire. Add a check increateNotificationto short-circuit when the user has opted out of a category.
Email digest (planned)
- A scheduled worker should
Notification.find({ userId, emailDigested: false, createdAt: { $gte: yesterday } }), batch by user, render a digest email viaemailService, markemailDigested: true. Not implemented today.
Sequence diagram
sequenceDiagram
autonumber
participant Svc as Originating Service<br/>(SellerOfferService / Webhook / ...)
participant NS as NotificationService
participant DB as MongoDB
participant IO as Socket.IO
participant FE as Frontend
actor U as User
Svc->>NS: createNotification({userId, title, message, ...})
NS->>DB: Notification.create
NS->>IO: emit user-{userId} 'new-notification'
IO-->>FE: 'new-notification' payload
FE-->>U: badge++, toast, prepend to list
U->>FE: open bell dropdown
FE->>NS: GET /api/notifications?page=1&limit=20
NS->>DB: Notification.find({userId}).sort({createdAt:-1})
NS-->>FE: { notifications, total, unreadCount }
U->>FE: click notification
FE->>NS: PATCH /api/notifications/{id}/read
NS->>DB: Notification.findOneAndUpdate(isRead:true)
NS->>IO: emit user-{userId} 'unread-count-update'
IO-->>FE: badge sync across tabs
FE-->>U: badge--, mark item as read
FE-->>U: navigate to notification.actionUrl
API calls
| Method | Endpoint | Purpose | Notes |
|---|---|---|---|
GET |
/api/notifications |
Paginated list with unreadCount |
|
GET |
/api/notifications/unread-count |
Just the unread count for badge | |
GET |
/api/notifications/:id |
Single notification | ⚠️ Known bug — see below |
PATCH |
/api/notifications/:id/read |
Mark single notification read | |
PATCH |
/api/notifications/mark-all-read |
Mark all notifications read | Previously documented incorrectly as POST /api/notifications/read-all |
DELETE |
/api/notifications/:id |
Remove from list |
⚠️ Known bug —
GET /api/notifications/:id: The backend controller does not perform a direct DB lookup by ID. Instead it callsgetUserNotifications(userId, 1, 1)(fetches only 1 record for the user) and then does an in-memory_idcomparison. Any notification that is not the user's single most-recent record will return404erroneously. Do not rely on this endpoint for arbitrary notification lookups until the controller is fixed to use a directfindOne({ _id, userId }).
Database writes
notifications— insert on create, update on read, delete on remove.- TTL: notifications are automatically deleted after 90 days via a MongoDB TTL index on
createdAt.
Socket events emitted
new-notification→user-{userId}. Payload includes the full notification document (so the frontend doesn't need to re-fetch).unread-count-update→user-{userId}. Emitted whenever the unread count changes (e.g. aftermarkAsReadormarkAllRead). Used for cross-tab and cross-device badge synchronisation. There is nonotification-readevent —unread-count-updateis the correct event to listen to for badge sync.level-up→user-{userId}fromPointsService.addPoints.referral-signup→user-{referrerId}from auth verify.chat-notification→user-{participantId}fromChatService.sendMessage(these are not stored in thenotificationscollection — they live alongside but drive only the chat-list badge).
Side effects
- Bell badge count is derived from
unreadCountreturned by the GET endpoint or computed client-side as items arrive. - Notification actions deep-link via
actionUrl(e.g./dashboard/buyer/requests/{id}). - Sentry breadcrumbs capture failed notification creations.
Error / edge cases
- User offline → notification is persisted in MongoDB; the user sees it when they next sign in. The socket emit is lossy (no replay).
- Multiple tabs / devices → the same
user-{id}room receives the event in each socket; all tabs update. Badge sync is driven byunread-count-update, not a per-itemnotification-readevent. - Disabled categories (planned) → service should early-return without DB write if the user has opted out, otherwise persist but don't push (so it shows in history but not as a toast).
- High volume (e.g. fan-out to thousands of sellers) → today every notification is a separate Mongo insert + socket emit. For mass announcements, consider
insertMany+ per-room broadcast. The 50ms stagger in Purchase Request Flow mitigates the worst case. - Stale unread count → if the frontend trusts a stale React Query cache, it can show wrong numbers; always reconcile against
unread-counton bell-icon open. - 90-day TTL → notifications older than 90 days are silently removed from MongoDB. The frontend should not assume a notification persists indefinitely.
[!tip] Always set
actionUrlEvery notification should have a deep-link target. Notifications withoutactionUrllead to dead clicks. The factory methods inNotificationService(e.g.notifyNewOfferReceived) already enforce this — keep the pattern when adding new helpers.
Linked flows
- Every other flow in this folder emits notifications via this service.
- Chat Flow — separate
chat-notificationsocket channel for chat badges.
Source files
- Backend:
backend/src/services/notification/NotificationService.ts - Backend:
backend/src/services/notification/notificationController.ts - Backend:
backend/src/services/notification/notificationControllerRoutes.ts - Backend:
backend/src/services/notification/routes.ts - Backend:
backend/src/models/Notification.ts - Frontend:
frontend/src/layouts/components/notifications-drawer/ - Frontend: socket provider (joins
user-{id}and listens fornew-notificationandunread-count-update)