--- title: Notification Flow tags: [flow, notification, socket-io, email] related_models: ["[[Notification]]", "[[User]]"] related_apis: ["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`). - **Chat** — `chat-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 / levels** — `level-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. - **Backend** — `NotificationService` (`backend/src/services/notification/NotificationService.ts`), routes at `/api/notifications`. - **MongoDB** — `notifications` 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 5. 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)`). 6. On event: increment the bell-icon badge, optionally show a toast (`notistack`), and prepend the entry into the cached notifications list (React Query cache). 7. The bell-icon dropdown also fetches `GET /api/notifications?page=1&limit=20` for paginated history. ### Reading 8. User opens the bell-icon dropdown — frontend calls `POST /api/notifications/mark-read` for each viewed entry (or `POST /api/notifications/read-all`). 9. `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 ```mermaid sequenceDiagram autonumber participant Svc as Originating Service
(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-notification`** → `user-{userId}`. Payload includes the full notification document (so the frontend doesn't need to re-fetch). - **`level-up`** → `user-{userId}` from `PointsService.addPoints`. - **`referral-signup`** → `user-{referrerId}` from auth verify. - **`chat-notification`** → `user-{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`)