Read receipts & unread

Each member of a conversation has a read cursor: the last message they have read. Unread counts, the app badge, clearing notifications on other devices, and the "read" check marks other people see all come from it.

The read cursor

A membership carries three fields:

FieldMeaning
last_read_message_idThe message the cursor points at.
last_read_sequenceThat message's sequence.
last_read_atWhen the cursor was set.

All three are null until the member reads something.

The cursor moves when:

  • The member sends a message. Their own message is read the moment it is sent, so it never counts as unread for them.
  • The app marks it read: POST /v1/conversations/:id/read with { "message_id": "…" }. In the SDK, markReadUpTo from useMessages, or markConversationRead from useConversations.
  • The member uses a notification: Reply or Mark as read (POST /v1/notifications/reply and /read). Both move the cursor to the newest message in the conversation.

POST …/read sets the cursor to exactly the message you name, which must be in that conversation. It does not check direction, so naming an older message moves the cursor back and the unread count goes up again. Always send the newest message the user has seen.

Unread counts

unread_count on a conversation (GET /v1/conversations and conversation:updated) is computed per user. It counts the conversation's messages that:

  • come after the user's cursor (sequence > last_read_sequence), and
  • are not deleted.

It includes thread replies and call-log messages. It is computed on the server, so it is the same on every device.

When a message is sent or deleted, every member gets a fresh count in conversation:updated on their user channel. Marking read does not send conversation:updated. Update your own badge locally, or refetch the list.

The app badge

Chat notifications carry unread_total: the user's unread messages across all their conversations. iOS shows it as the app badge. It is the sum of the same count as unread_count over all the user's conversations, so deleted messages do not count. A read push updates it when the user reads a conversation.

Read receipts: member:read

When a cursor moves, poolse pushes member:read on the conversation channel:

{
  "user_id": "9c41…",
  "conversation_id": "6f1c…",
  "last_read_message_id": "8d0e…",
  "last_read_at": "2026-10-27T09:15:40.000000Z"
}

Mark a message as read by a member when its sequence is at or below that member's last_read_sequence. GET /v1/conversations/:id/members gives every member's cursor when you load the conversation.

Only read receipts exist. poolse has no "delivered" state.

The workspace setting

Choose who can see read receipts on the dashboard's Messaging page:

ModeEffect
on (default)Everyone sees everyone's read state.
offNobody sees anyone else's. Show no check marks.
userEach user decides for themselves. The rule is reciprocal, as in WhatsApp: someone who stops sharing their read receipts also stops seeing other people's.

The setting only hides other people's read state. A user's own cursor always works: their unread counts, their badge, and clearing notifications on their other devices are not affected.

poolse enforces the setting on the server, so a modified client cannot see what it hides:

  • member:read reaches the reader's own devices always. Others get it only when the setting lets them see it (see who receives it).
  • GET /v1/conversations/:id/members returns null cursor fields for members the caller may not see.

What the current user may do

GET /v1/me includes read_receipts:

"read_receipts": { "mode": "user", "shared": true, "can_change": true, "visible": true }
FieldMeaning
modeThe workspace setting: on, off or user.
sharedWhether others can see this user's read state.
can_changetrue only in user mode.
visibleWhether this user sees other people's read state. When false, hide every check mark.

In user mode, a user changes their choice with PATCH /v1/me { "read_receipts": false }. In the other modes that call fails with 409 read_receipts_fixed. See Current user.

In React, useReadReceipts() returns { visible, mode, shared, canChange, setShared }.

Read receipts & unread · Concepts · poolse docs