Appearance
Notifications
Planner uses real-time notifications delivered via WebSocket (Socket.IO). The NotificationsModule is @Global(), so any service can inject NotificationsService directly.
Architecture
- Namespace:
/notifications - Auth: Token-based via
handshake.auth.tokenusingJwtService - Rooms: Users join
user:<id>room on connection - Delivery: Emitted as
notification:newevent to the user's room
Notification Types
| Type | Trigger | Data |
|---|---|---|
task.assigned | Task assigned to user | { taskId, projectId } |
task.status_changed | Task status updated | { taskId, projectId, oldStatus, newStatus } |
task.comment_added | New comment or reply | { taskId, projectId, commentId } |
team.member.added | User added to team | { teamId, teamSlug } |
org.member.added | User added to org | { orgId } |
project.member.added | User added to project | { projectId } |
API Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/notifications | List user's notifications |
GET | /api/notifications/unread-count | Unread count |
PATCH | /api/notifications/:id/read | Mark as read |
PATCH | /api/notifications/read-all | Mark all as read |
DELETE | /api/notifications/:id | Delete notification |
Prisma Model
prisma
model Notification {
id String @id @default(uuid())
userId String
type String
title String
body String?
data Json? // Arbitrary event data for deep-linking
readAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@index([userId, readAt])
@@index([userId, createdAt])
}Frontend
Notification Store
typescript
const store = useNotificationsStore()
const { notifications, unreadCount, loading } = storeToRefs(store)
await store.fetchNotifications()
await store.fetchUnreadCount()
await store.markAsRead(id)
await store.markAllAsRead()
await store.deleteNotification(id)Socket Listener
The frontend connects to the /notifications namespace on auth and listens for notification:new events:
typescript
socket.on('notification:new', (notification) => {
notifications.value.unshift(notification)
unreadCount.value++
})Key Implementation Details
NotificationsService.create()is fire-and-forget (not awaited) — it doesn't slow down the main operation- The
dataJSON field stores structured payload (taskId, projectId, etc.) for deep-linking from the notification center - Comment replies notify the parent comment author (not just task creator/assignee)
- Unread count is displayed as a badge in the header