Unread/Read State Tracking
Why it matters
Without a read-state column, every notification looks identical forever — the bell badge never clears, users learn to ignore it, and push/email digests repeat content the user already saw in-app. A single read_at timestamp unlocks unread counts, mark-all-as-read, per-channel deduplication across web and mobile, and analytics on notification effectiveness. Missing this field means every downstream notification feature is blocked until the schema is backfilled.
Severity rationale
Low because notifications still deliver, but engagement quality and cross-channel deduplication are blocked without the field.
Remediation
Add a nullable readAt DateTime? column to the notification model, expose PATCH endpoints for single-item and bulk mark-as-read, and compute the unread badge from notifications.filter(n => !n.readAt).length. Store the timestamp (not a boolean) so you can audit read latency later. See prisma/schema.prisma and app/api/notifications/[id]/route.ts:
model Notification {
id String @id
readAt DateTime?
userId String
}
Detection
-
ID:
read-state-management -
Severity:
low -
What to look for: Enumerate all relevant files and Examine the notification schema. Look for a
read_attimestamp oris_readboolean field. Check for API endpoints to "Mark as read" (single or bulk). Look for UI badges showing unread notification count. -
Pass criteria: No more than 0 violations are acceptable. Notification model includes a read state field. Endpoints exist to mark notifications as read. UI displays unread count and visual indicators for unread notifications.
-
Fail criteria: No read state tracked, or notifications are always read, or no way to mark as read.
-
Skip (N/A) when: Notifications feature doesn't exist.
-
Detail on fail:
"Notification model has no read_at field — cannot track read state"or"UI shows notification count but no 'Mark as read' button" -
Remediation: Add read state tracking:
// Prisma schema model Notification { id String @id userId String type String // 'post_liked', 'user_followed', etc. content String readAt DateTime? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) } // API route to mark as read export async function PATCH(req: Request, { params }) { const { read } = await req.json() const notification = await db.notification.update({ where: { id: params.id }, data: { readAt: read ? new Date() : null } }) return Response.json(notification) } // Mark all as read export async function PUT(req: Request) { const userId = getCurrentUserId() await db.notification.updateMany({ where: { userId, readAt: null }, data: { readAt: new Date() } }) return Response.json({ success: true }) } // UI component function NotificationBell() { const unreadCount = notifications.filter(n => !n.readAt).length return ( <button> 🔔 {unreadCount > 0 && <span className="badge">{unreadCount}</span>} </button> ) }
Taxons
History
- 2026-04-18·v1.0.0·Initial import from community-social-engagement·automated