Files
nick-doc/04 - Flows/Notification Flow.md
Siavash Sameni 7a616744f4 docs: complete code-reality alignment for remaining docs + reconcile issue set
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>
2026-05-29 15:15:02 +04:00

10 KiB

title, tags, related_models, related_apis
title tags related_models related_apis
Notification Flow
flow
notification
socket-io
email
Notification
User
GET /api/notifications
PATCH /api/notifications/:id/read
PATCH /api/notifications/mark-all-read
DELETE /api/notifications/:id

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).
  • Chatchat-notification events (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 / levelslevel-up event 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.
  • BackendNotificationService (backend/src/services/notification/NotificationService.ts), routes at /api/notifications.
  • MongoDBnotifications collection (one document per notification). Notifications are auto-deleted after 90 days (TTL index on createdAt).
  • Socket.IO — emits new-notification to user-{userId}.
  • Email (optional) — periodic digest worker (not implemented today; planned).

Step-by-step narrative

Creating a notification

  1. Any service builds a NotificationCreateData object:
    {
      userId, title, message,
      type: 'info' | 'success' | 'warning' | 'error',
      category: 'purchase_request' | 'offer' | 'payment' | 'delivery' | 'system',
      relatedId?, metadata?, actionUrl?
    }
    
  2. Calls await notificationService.createNotification(data).
  3. NotificationService.createNotification (NotificationService.ts:18-37):
    • Notification.create({ ...data, isRead: false, createdAt: now }).
    • Calls this.emitRealTimeNotification(userId, saved) which global.io.to('user-${userId}').emit('new-notification', payload).
  4. The notification is persisted and pushed simultaneously.

Frontend reception

  1. The frontend's global Socket.IO provider listens for new-notification events on its user-{me} room (joined on app mount via socket.emit('join-user-room', userId)).
  2. On event: increment the bell-icon badge, optionally show a toast (notistack), and prepend the entry into the cached notifications list (React Query cache).
  3. The bell-icon dropdown also fetches GET /api/notifications?page=1&limit=20 for paginated history.

Reading

  1. User opens the bell-icon dropdown — frontend calls PATCH /api/notifications/:id/read for each viewed entry, or PATCH /api/notifications/mark-all-read to clear all at once.
  2. NotificationService.markAsRead(notificationId, userId) (NotificationService.ts:74-90):
    • Notification.findOneAndUpdate({ _id, userId }, { isRead: true, readAt: now }).
    • After updating, the backend emits unread-count-update to user-{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 in createNotification to 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 via emailService, mark emailDigested: 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 calls getUserNotifications(userId, 1, 1) (fetches only 1 record for the user) and then does an in-memory _id comparison. Any notification that is not the user's single most-recent record will return 404 erroneously. Do not rely on this endpoint for arbitrary notification lookups until the controller is fixed to use a direct findOne({ _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-notificationuser-{userId}. Payload includes the full notification document (so the frontend doesn't need to re-fetch).
  • unread-count-updateuser-{userId}. Emitted whenever the unread count changes (e.g. after markAsRead or markAllRead). Used for cross-tab and cross-device badge synchronisation. There is no notification-read event — unread-count-update is the correct event to listen to for badge sync.
  • level-upuser-{userId} from PointsService.addPoints.
  • referral-signupuser-{referrerId} from auth verify.
  • chat-notificationuser-{participantId} from ChatService.sendMessage (these are not stored in the notifications collection — they live alongside but drive only the chat-list badge).

Side effects

  • Bell badge count is derived from unreadCount returned 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 by unread-count-update, not a per-item notification-read event.
  • 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-count on 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 actionUrl Every notification should have a deep-link target. Notifications without actionUrl lead to dead clicks. The factory methods in NotificationService (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-notification socket 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 for new-notification and unread-count-update)