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.
Low because notifications still deliver, but engagement quality and cross-channel deduplication are blocked without the field.
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
}
ID: community-social-engagement.notifications.read-state-management
Severity: low
What to look for: Enumerate all relevant files and Examine the notification schema. Look for a read_at timestamp or is_read boolean 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>
)
}