Notifications
In-app notification center + email notifications for marketplace events
Build on the event seam
The backend seam for this feature is specified in Operations §4: domain services emit events (deal.status.changed, message created) via @nestjs/event-emitter; one listener module owns mailer templates and, later, the Notification table below — without touching domain services again.
Current state
- Password reset emails via Resend/SMTP
- Real-time chat via Socket.IO (per-user unread pushes already consumed by the web app)
- No in-app notifications, no deal event emails
Channels
| Channel | Use case | Priority |
|---|---|---|
| In-app (bell icon) | All events | V1 |
| Deal status changes, new messages (if offline), payment events | V1 | |
| Push (web) | New messages, deal sold, payment received | V2 |
Data model
model Notification {
id Int @id @default(autoincrement())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
type NotificationType
title String
body String
data Json? // { dealId: 123, transactionId: 456, etc. }
read Boolean @default(false)
createdAt DateTime @default(now())
@@index([userId, read])
@@index([userId, createdAt])
}
enum NotificationType {
DEAL_APPROVED
DEAL_DECLINED
DEAL_SOLD
DEAL_EXPIRED
PAYMENT_RECEIVED
PAYMENT_RELEASED
PAYMENT_REFUNDED
VERIFICATION_APPROVED
VERIFICATION_REJECTED
NEW_MESSAGE
PACKAGE_SHIPPED
PACKAGE_DELIVERED
SYSTEM_ANNOUNCEMENT
}
model NotificationPreference {
userId String @id
user User @relation(fields: [userId], references: [id])
emailDeals Boolean @default(true)
emailMessages Boolean @default(true)
emailPayments Boolean @default(true)
emailDigest Boolean @default(true) // batch emails instead of individual
}Event → notification matrix
| Event | Who | Channel |
|---|---|---|
| Deal approved / declined by admin | Seller | In-app + Email |
| Someone buys your deal | Seller | In-app + Email |
| Payment confirmed | Buyer + Seller | In-app + Email |
| New chat message (offline 5min+) | Recipient | In-app + Email |
| Package shipped / delivered | Buyer (+ Seller on delivery) | In-app (+ Email for shipped) |
| Funds released / refund issued | Seller / Buyer | In-app + Email |
| Verification approved / rejected | Seller | In-app (+ Email on reject) |
| Deal about to expire (7d) | Seller | In-app + Email |
Architecture
NotificationService
├─ create(userId, type, title, body, data)
│ ├─ Save to DB
│ ├─ Emit via WebSocket (if user online — user:{id} room already exists)
│ └─ Queue email (if user offline or email-worthy event)
├─ markAsRead(userId, notificationId)
├─ markAllAsRead(userId)
└─ getUnread(userId) → count + list
EmailQueue (BullMQ — Redis already present)
├─ Process with Resend API
├─ Rate limiting (avoid spam)
└─ Batch digest: if 5+ unread after 1h, send single digest emailTasks
Backend: Notification model; NotificationService (create/read/markAsRead); REST endpoints; WebSocket emission on create; email queue (BullMQ + Resend); email templates; digest cron; preferences endpoint; hook into the event listeners from the operations seam — not into domain services directly.
Frontend: bell icon in header with unread badge; notification dropdown/panel; mark-as-read on click (navigates to the relevant page); "mark all as read"; preferences page in profile; real-time delivery via the existing Socket.IO connection.