Inferring read status from message delivery lies to users about what their conversation partners have actually seen. A message marked read the instant it lands on a device claims acknowledgment the recipient never gave, which breaks trust in the product, corrupts engagement analytics built on receipt events, and creates evidentiary problems in regulated workflows (harassment reports, support SLAs, legal discovery) where read receipts are treated as proof the user saw the content. Data-integrity failures here cascade: downstream features like unread counts, notification suppression, and retention funnels all key off a signal that does not represent reality.
Medium because the integrity breach misleads users and distorts analytics but does not by itself expose credentials or enable account takeover.
Require a deliberate client action before recording a read receipt and persist the receipt only in response to that action. Wire a mark_as_read socket event that fires when the message enters the viewport or the user focuses the conversation, then write the receipt in your realtime handler (for example src/server/socket/handlers/read-receipts.ts) and fan out to other participants.
socket.on('mark_as_read', async ({ messageId, channel }) => {
await db.readReceipts.create({ messageId, userId: socket.userId, channel, timestamp: new Date() });
io.to(channel).emit('read_receipt', { messageId, userId: socket.userId, timestamp: new Date() });
});
ID: community-realtime.presence.read-receipts-explicit
Severity: medium
What to look for: Enumerate all read receipt storage paths. For each, classify whether receipts are stored as explicit user acknowledgment actions or inferred from delivery. Count the paths that are explicit vs. inferred.
Pass criteria: 100% of read receipts are stored based on explicit user acknowledgment, not inferred from message delivery. The acknowledgment must require at least 1 deliberate user action.
Fail criteria: Read status is inferred from delivery (e.g., "message reached client"), not from explicit user action.
Skip (N/A) when: The platform does not implement read receipts.
Detail on fail: "Read status inferred from delivery. Message marked read immediately upon reaching client, not when user acknowledges it."
Remediation: Implement explicit read receipt handling:
socket.on('mark_as_read', async (data: { messageId: string; channel: string }) => {
await db.readReceipts.create({
messageId: data.messageId,
userId: socket.userId,
channel: data.channel,
timestamp: new Date(),
});
// Notify others that this user read the message
io.to(data.channel).emit('read_receipt', {
messageId: data.messageId,
userId: socket.userId,
timestamp: new Date(),
});
});