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:
| Field | Meaning |
|---|---|
last_read_message_id | The message the cursor points at. |
last_read_sequence | That message's sequence. |
last_read_at | When 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/readwith{ "message_id": "…" }. In the SDK,markReadUpTofromuseMessages, ormarkConversationReadfromuseConversations. - The member uses a notification: Reply or Mark as read
(
POST /v1/notifications/replyand/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:
| Mode | Effect |
|---|---|
on (default) | Everyone sees everyone's read state. |
off | Nobody sees anyone else's. Show no check marks. |
user | Each 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:readreaches 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/membersreturnsnullcursor 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 }
| Field | Meaning |
|---|---|
mode | The workspace setting: on, off or user. |
shared | Whether others can see this user's read state. |
can_change | true only in user mode. |
visible | Whether 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 }.