Files
nick-doc/04 - Flows/Notification Flow.md
2026-05-23 20:35:34 +03:30

8.2 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
POST /api/notifications/read-all
DELETE /api/notifications/:id

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).
  • 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 POST /api/notifications/mark-read for each viewed entry (or POST /api/notifications/read-all).
  2. NotificationService.markAsRead(notificationId, userId) (NotificationService.ts:74-90):
    • Notification.findOneAndUpdate({ _id, userId }, { isRead: true, readAt: now }).
    • Emits notification-read (or recomputes unread count) so other open tabs sync.

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)
    FE-->>U: badge--, mark item as read
    FE-->>U: navigate to notification.actionUrl

API calls

Method Endpoint Purpose
GET /api/notifications Paginated list with unreadCount
GET /api/notifications/unread-count Just the unread count for badge
PATCH /api/notifications/:id/read Mark single notification read
POST /api/notifications/read-all Mark all read
DELETE /api/notifications/:id Remove from list

Database writes

  • notifications — insert on create, update on read, delete on remove.

Socket events emitted

  • new-notificationuser-{userId}. Payload includes the full notification document (so the frontend doesn't need to re-fetch).
  • 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.
  • 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.

[!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)