User channel
topic = "user:<user_id>"
<user_id> is the poolse id of the signed-in user (from
GET /v1/me), not your external_id. A user can only join their own
topic. Any other id is refused with forbidden.
Keep this channel joined for as long as the user is signed in. Unlike
a conversation channel, it does not depend on which screen is open.
poolse.realtime.user(id) returns it, and useConversations() joins
it for you.
Client → server
Nothing. Every event a client sends here gets an error reply:
{ "status": "error", "response": { "reason": "unknown_event" } }
Server → client
| Event | When |
|---|---|
conversation:created | The user created a conversation or was added to one. |
conversation:updated | A message was sent or deleted, or the latest message edited, in one of the user's conversations. |
mention:new | A message mentioned the user. |
call:ring_ended | This user's ring for a call is over. |
call:finished | A call this user was rung for, or was in, has ended. |
conversation:created
The user is in a new conversation: they created it, or someone added them. Add it to the inbox.
{
"id": "6f1c…",
"tenant_id": "6578…",
"type": "group",
"name": "Engineering",
"avatar_url": null,
"created_by_user_id": "2b7e…",
"member_limit": null,
"custom_data": {},
"settings": {},
"last_message_at": null,
"last_message_preview": null,
"last_sequence": 0,
"unread_count": null,
"inserted_at": "2026-10-27T09:00:00.000000Z",
"updated_at": "2026-10-27T09:00:00.000000Z",
"member_external_ids": ["acme-alice", "acme-bob"]
}
The conversation, as GET /v1/conversations/:id returns it.
unread_count is null here; treat it as 0.
When a conversation is created (POST /v1/conversations), every
member, the creator included, gets this event once the conversation
has been saved. Each copy lists every member in member_external_ids.
If creating fails, nobody gets it. A member added later
(POST /v1/conversations/:id/members) gets it at that point, listing
the members at that moment.
conversation:updated
Sent to every member when, in a conversation they belong to, a
message is sent, a message is deleted, or the latest message is edited
(its last_message_preview changes). Use it to update the inbox row and its
unread badge.
{
"conversation": {
"id": "6f1c…",
"type": "group",
"name": "Engineering",
"last_message_at": "2026-10-27T09:14:03.112233Z",
"last_message_preview": "ship it",
"last_sequence": 185,
"unread_count": 3,
"member_external_ids": ["acme-alice", "acme-bob", "acme-carol"],
"…": "every other conversation field"
},
"by_user_id": "2b7e…"
}
| Field | Type | Meaning |
|---|---|---|
conversation | object | The full conversation, with unread_count computed for this recipient. |
conversation.unread_count | integer | Messages in the conversation that are not deleted and come after this user's read cursor. The sender's own message never counts: sending moves the sender's cursor. |
conversation.last_message_preview | string | null | Up to 80 characters of the newest message, Attachment / N attachments for one without text, or the call-record JSON for a call-log message. |
by_user_id | uuid | null | Who caused the update: the sender, the person who deleted a message, or the editor. null for a call-log message. |
Not sent when the conversation is renamed or its custom_data
changes. Not sent when someone marks the conversation read either.
That comes on the conversation channel as member:read, and the
reader's next GET /v1/conversations has the new count.
mention:new
A message mentioned this user.
{ "message_id": "8d0e…", "conversation_id": "6f1c…", "sender_id": "2b7e…" }
Poolse ids only. Fetch the message from the conversation if you need
its text. No hook or component in the SDK uses this event. Listen with
poolse.realtime.user(id).onMention(cb).
call:ring_ended
This user's phone should stop ringing for a call, because it was answered, declined, cancelled, timed out or finished.
{
"cv": 1,
"call_id": "4a69…",
"conversation_id": "6f1c…",
"reason": "answered_elsewhere",
"device_id": "d3a0…"
}
| Field | Type | Meaning |
|---|---|---|
reason | string | answered_elsewhere (this user picked up on one device), declined (this user declined on one device), cancelled (the caller hung up first), timeout (it rang out), or finished (the call ended while it rang). |
device_id | uuid | Only present with answered_elsewhere, and only when the call was answered from a ring push: the registered device that answered. That device should ignore the event. |
If you use @poolse/native, pass this on to the native core:
onRingEnded(call_id, reason). See
Ringing phones end to end.
call:finished
A call this user was rung for or took part in has ended. That includes the caller, everyone rung (also people added later) and everyone who joined.
{
"cv": 1,
"call_id": "4a69…",
"conversation_id": "6f1c…",
"ended_at": "2026-10-27T09:34:25.000Z",
"duration_ms": 265000,
"joined_user_ids": ["2b7e…", "9c41…"],
"reason": "finished"
}
The same fields as
call:finished on the conversation channel,
plus conversation_id. It is sent here as well because a phone often
leaves the conversation's channel when the user navigates away during
a call. With @poolse/native, pass it on: onCallFinished(call_id, reason).
Not on this channel
- Presence: poolse has no app-wide online status. Presence is per
conversation (
presence_state/presence_diffon the conversation channel). - Being removed from a conversation: there is no event here. The
conversation's own channel closes instead (
phx_close). - An incoming call: rings go out by push. A
foreground app sees the call through
call:startedon the conversation channel.