Files

891 lines
27 KiB
Plaintext

---
id: 'protocol'
title: 'Realtime Protocol'
description: 'Understanding Realtime Protocol'
---
## WebSocket connection setup
To start the connection we use the WebSocket URL, which for:
- Supabase projects: `wss://<PROJECT_REF>.supabase.co/realtime/v1/websocket?apikey=<API_KEY>`
- self-hosted projects: `wss://<HOST>:<PORT>/socket/websocket?apikey=<API_KEY>`
{/* supa-mdx-lint-disable-next-line Rule003Spelling */}
As an example, using [websocat](https://github.com/vi/websocat), you would run the following command in your terminal:
```bash
# With Supabase
websocat "wss://<PROJECT_REF>.supabase.co/realtime/v1/websocket?apikey=<API_KEY>"
# With self-hosted
websocat "wss://<HOST>:<PORT>/socket/websocket?apikey=<API_KEY>"
```
During this stage you can also set other URL params:
- `vsn`: sets the protocol version. Possible values are `1.0.0` and `2.0.0`. Defaults to `1.0.0`.
- `log_level`: sets the log level to be used by this connection to help you debug potential issues. This only affects server side logs.
After connecting a `phx_join` event must be sent to the server to join a channel. The next sections outline the different messages types and events that are supported.
## Protocol messages
Messages can be serialized in different formats. The Realtime protocol supports two versions: `1.0.0` and `2.0.0`.
## 1.0.0
Version 1.0.0 is extremely simple. It uses JSON as the serialization format for messages. The underlying WebSocket messages are all text frames.
Messages contain the following fields:
- `event`: The type of event being sent or received. Example `phx_join`, `postgres_changes`, `broadcast`, etc.
- `topic`: The topic to which the message belongs. This is a string that identifies the channel or context of the message.
- `payload`: The data associated with the event. This can be any JSON-serializable data structure, such as an object or an array.
- `ref`: A unique reference ID for the message. This is useful to track replies to a specific message.
- `join_ref`: A unique reference ID to uniquely identify a joined topic for pushes, broadcasts, replies, etc.
Example:
```json
{
"topic": "realtime:presence-room",
"event": "phx_join",
"payload": {
"config": {
"broadcast": {
"ack": false,
"self": false
},
"presence": {
"enabled": false
},
"private": false
}
},
"ref": "1",
"join_ref": "1"
}
```
## 2.0.0
Version 2.0.0 uses text and binary WebSocket frames.
### Text frames
Text frames are always JSON encoded, but unlike version 1.0.0, they use a JSON array where the element order must be exactly:
- `join_ref`
- `ref`
- `topic`
- `event`
- `payload`
Example:
```json
[
"1",
"1",
"realtime:presence-room",
"phx_join",
{
"config": {
"broadcast": {
"ack": false,
"self": false
},
"presence": {
"enabled": false
},
"private": false
}
}
]
```
### Binary frames
The two special message types have a well defined binary format where the first byte defines the type of message. Both are used to send and receive broadcast events. See the [client](#client-sent-events) and [server](#server-sent-events) sent events for more details.
| Code | Type | Description |
| ---- | ------------------- | ----------------------------- |
| 3 | USER_BROADCAST_PUSH | User-initiated broadcast push |
| 4 | USER_BROADCAST | User broadcast message |
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### User Broadcast Push
```
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type (0x03) | Join Ref Size | Ref Size | Topic Size |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|User Event Size| Metadata Size | Payload Enc. | Join Ref ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Ref (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Topic (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User Event (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Metadata (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User Payload (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
**Field Descriptions:**
- **Type**: 1 byte, value = 0x03
- **Join Ref Size**: 1 byte, size of join reference string (max 255)
- **Ref Size**: 1 byte, size of reference string (max 255)
- **Topic Size**: 1 byte, size of topic string (max 255)
- **User Event Size**: 1 byte, size of user event string (max 255)
- **Metadata Size**: 1 byte, size of metadata string (max 255)
- **Payload Encoding**: 1 byte (0 = binary, 1 = JSON)
- **Join Ref**: Variable length string
- **Ref**: Variable length string
- **Topic**: Variable length string
- **User Event**: Variable length string
- **Metadata**: Variable length JSON string
- **User Payload**: Variable length payload data
#### User Broadcast
```
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type (0x04) | Topic Size |User Event Size| Metadata Size |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload Enc. | Topic (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User Event (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Metadata (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User Payload (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
**Field Descriptions:**
- **Type**: 1 byte, value = 0x04
- **Topic Size**: 1 byte, size of topic string (max 255)
- **User Event Size**: 1 byte, size of user event string (max 255)
- **Metadata Size**: 1 byte, size of metadata JSON string (max 255)
- **Payload Encoding**: 1 byte (0 = binary, 1 = JSON)
- **Topic**: Variable length string
- **User Event**: Variable length string
- **Metadata**: Variable length JSON string
- **User Payload**: Variable length payload data
## Event types
Messages for all events are encoded as text frames using JSON except with the `broadcast` event type which can happen on both text and binary frames.
### Client sent events
| Event Type | Description | Requires Ref | Requires Join Ref |
| -------------- | -------------------------------------------------------- | ------------ | ----------------- |
| `phx_join` | Initial message to join a channel and configure features | ✅ | ✅ |
| `phx_leave` | Message to leave a channel | ✅ | ✅ |
| `heartbeat` | Heartbeat message to keep the connection alive | ✅ | ⛔ |
| `access_token` | Message to update the access token | ✅ | ✅ |
| `broadcast` | Broadcast message sent to all clients in a channel | ✅ | ✅ |
| `presence` | Presence state update sent after joining a channel | ✅ | ✅ |
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### phx_join
This is the initial message required to join a channel. The client sends this message to the server to join a specific topic and configure the features it wants to use, such as Postgres changes, Presence, and Broadcast. The payload of the `phx_join` event contains the configuration options for the channel.
```ts
{
"config": {
"broadcast": {
"ack": boolean,
"self": boolean,
"replay" : {
"since": integer,
"limit": integer
}
},
"presence": {
"enabled": boolean,
"key": string
},
"postgres_changes": [
{
"event": string,
"schema": string,
"table": string,
"filter": string
}
]
"private": boolean
},
"access_token": string
}
```
- `config`:
- `private`: Whether the channel is private
- `broadcast`: Configuration options for broadcasting messages
- `ack`: Acknowledge broadcast messages
- `self`: Include the sender in broadcast messages
- `replay`: Configuration options for broadcast replay (Optional)
- `since`: Replay messages since a specific timestamp in milliseconds
- `limit`: Limit the number of replayed messages (Optional)
- `presence`: Configuration options for presence tracking
- `enabled`: Whether presence tracking is enabled for this channel
- `key`: Key to be used for presence tracking, if not specified or empty, a UUID will be generated and used
- `postgres_changes`: Array of configurations for Postgres changes
- `event`: Database change event to listen to, accepts `INSERT`, `UPDATE`, `DELETE`, or `*` to listen to all events.
- `schema`: Schema of the table to listen to, accepts `*` wildcard to listen to all schemas
- `table`: Table of the database to listen to, accepts `*` wildcard to listen to all tables
- `filter`: Filter to be used when pulling changes from database. Read more about filters in the usage docs for [Postgres Changes](/docs/guides/realtime/postgres-changes?queryGroups=language&language=js#filtering-for-specific-changes)
- `access_token`: Optional access token for authentication, if not provided, the server will use the API key.
Example on protocol version `2.0.0`:
```json
[
"3",
"5",
"realtime:chat-room",
"phx_join",
{
"config": {
"broadcast": {
"ack": false,
"self": true,
"replay": {
"since": 1763407103911,
"limit": 10
}
},
"presence": {
"key": "user_id-827",
"enabled": true
},
"postgres_changes": [],
"private": true
}
}
]
```
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### phx_leave
This message is sent by the client to leave a channel. It can be used to clean up resources or stop listening for events on that channel. Payload should be empty object.
Example on protocol version `2.0.0`:
```json
["1", "3", "realtime:avatar-stack-demo", "phx_leave", {}]
```
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### heartbeat
The heartbeat message should be sent at least every 25 seconds to avoid a connection timeout. Payload should be an empty object.
For heartbeat, the topic `phoenix` is used as this special message is not connected to a specific channel.
Example on protocol version `2.0.0`:
```json
[null, "26", "phoenix", "heartbeat", {}]
```
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### access_token
Used to setup a new token to be used by Realtime for authentication and to refresh the token to prevent a private channel from closing when the token expires.
```ts
{
"access_token": string
}
```
- `access_token`: The new access token to be used for authentication. Either to change it or to refresh it.
Example on protocol version `2.0.0`:
```json
[
"10",
"1",
"realtime:chat-room",
"access_token",
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
}
]
```
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### broadcast (text frame)
Used to send a broadcast event to all clients in a channel.
The `payload` field contains the event name and the data to broadcast.
```ts
{
"event": string,
"payload": json,
"type": "broadcast"
}
```
- `event`: The name of the user event to broadcast.
- `payload`: The user data associated with the event, which can be any JSON-serializable data structure.
- `type`: The type of message, which must always be `broadcast`.
Example on protocol version `2.0.0`:
```json
[
"10",
"1",
"realtime:chat-room",
"broadcast",
{
"event": "user-event",
"type": "broadcast",
"payload": {
"content": "Hello, World!",
"createdAt": "2025-11-17T21:14:14Z",
"id": "9b823349-71c0-465b-9a83-a63aa2a9ae6d",
"username": "VCSHLD556nQD-B-vUTJJ3"
}
}
]
```
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### broadcast (binary frame)
See the [User Broadcast Push](#user-broadcast-push) section for the binary frame structure.
This message is a streamlined version of the text frame broadcast event that also supports non-JSON payloads.
Below is the same example from the previous section, showing the binary frame structure with hexadecimal values for the header and plain text for the remaining fields:
- Join Ref: `10`
- Ref: `1`
- Topic: `realtime:chat-room`
- Payload encoding being JSON
- User Event: `user-event`
- Metadata is empty
- User Payload
```
0x03 // Type
0x02 // Join Ref Size
0x01 // Ref Size
0x12 // Topic Size
0x0A // User Event Size
0x00 // Metadata Size
0x01 // Payload Encoding (1 = JSON)
10 // Actual Join Ref
1 // Actual Ref
realtime:chat-room // Topic
user-event // User Event
{ // User Event Payload
"content": "Hello, World!",
"createdAt": "2025-11-17T21:14:14Z",
"id": "9b823349-71c0-465b-9a83-a63aa2a9ae6d",
"username": "VCSHLD556nQD-B-vUTJJ3"
}
```
The payload encoding is just a hint for the client to know if the payload should be treated as JSON or not.
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### presence
Used to send presence metadata after joining a channel. The payload contains the presence information to be tracked by the server.
This metadata is then sent back to all clients in the channel via `presence_state` and `presence_diff` events.
```ts
{
"type": "presence",
"event": "track",
"payload": json
}
```
Example on protocol version `2.0.0`:
```json
[
"1",
"5",
"realtime:presence-room",
"presence",
{
"type": "presence",
"event": "track",
"payload": {
"name": "Alice",
"color": "hsl(29, 100%, 70%)"
}
}
]
```
### Server sent events
| Event Type | Description | Requires Ref | Requires Join Ref |
| ------------------ | ----------------------------------------------------------------------- | ------------ | ----------------- |
| `phx_close` | Message from server to signal channel closed | ✅ | ✅ |
| `phx_error` | Error message sent by the server when an error occurs | ✅ | ✅ |
| `phx_reply` | Response to a `phx_join` or other requests | ✅ | ✅\* |
| `system` | System messages to inform about the status of the Postgres subscription | ⛔ | ⛔ |
| `broadcast` | Broadcast message sent to all clients in a channel | ⛔ | ⛔ |
| `presence_state` | Presence state sent by the server on join | ⛔ | ⛔ |
| `presence_diff` | Presence state diff update sent after a change in presence state | ⛔ | ⛔ |
| `postgres_changes` | Postgres CDC message containing changes to the database | ⛔ | ⛔ |
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### phx_close
This message is sent by the server to signal that the channel has been closed. Payload will be empty object.
Example on protocol version `2.0.0`:
```json
["3", "3", "realtime:avatar-stack-demo", "phx_close", {}]
```
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### phx_error
This message is sent by the server when an unexpected error occurs in the channel. Payload will be an empty object
```json
["3", "3", "realtime:avatar-stack-demo", "phx_error", {}]
```
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### phx_reply
The server sends these messages in response to client requests that require acknowledgment.
```ts
{
"status": string,
"response": any,
}
```
- `status`: The status of the response, can be `ok` or `error`.
- `response`: The response data, which can vary based on the event that was replied to
`phx_join` has a specific response structure outlined below.
Contains the status of the join request and any additional information requested in the `phx_join` payload.
```ts
{
"postgres_changes": [
{
"id": number,
"event": string,
"schema": string,
"table": string
}
]
}
```
- `postgres_changes`: Array of Postgres changes that the client is subscribed to, each object contains:
- `id`: Unique identifier for the Postgres changes subscription
- `event`: The type of event the client is subscribed to, such as `INSERT`, `UPDATE`, `DELETE`, or `*`
- `schema`: The schema of the table the client is subscribed to
- `table`: The table the client is subscribed to
Example on protocol version `2.0.0`:
```json
[
"1",
"1",
"realtime:chat-room",
"phx_reply",
{
"status": "ok",
"response": {
"postgres_changes": [
{
"id": 106243155,
"event": "*",
"schema": "public",
"table": "test"
}
]
}
}
]
```
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### system
The server sends system messages to inform clients about the status of their Realtime channel subscriptions.
```ts
{
"message": string,
"status": string,
"extension": string,
"channel": string
}
```
- `message`: A human-readable message describing the status of the subscription.
- `status`: The status of the subscription, can be `ok`, `error`, or `timeout`.
- `extension`: The extension that sent the message.
- `channel`: The channel to which the message belongs, such as `realtime:room1`.
Example on protocol version `2.0.0`:
```json
[
"13",
null,
"realtime:chat-room",
"system",
{
"message": "Subscribed to PostgreSQL",
"status": "ok",
"extension": "postgres_changes",
"channel": "main"
}
]
```
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### broadcast (text frame)
{/* supa-mdx-lint-disable-next-line Rule003Spelling */}
This is the structure of broadcast events received by all clients subscribed to a channel. The `payload` field contains the event name and data that was broadcasted.
```ts
{
"event": string,
"meta" : {
"id" : uuid,
"replayed" : boolean
},
"payload": json,
"type": "broadcast"
}
```
- `event`: The name of the user event to broadcast.
- `meta`: Metadata about the broadcast message. Not always present.
- `id`: A unique identifier for the broadcast message in UUID format.
- `replayed`: A boolean indicating whether the message is a replayed message. Not always present
- `payload`: The user data associated with the event, which can be any JSON-serializable data structure.
- `type`: The type of message, which must always be `broadcast` for broadcast messages.
Example on protocol version `2.0.0`:
```json
[
null,
null,
"realtime:chat-room",
"broadcast",
{
"event": "message",
"type": "broadcast",
"meta": {
"id": "006554ce-d22d-469c-877a-88bef47214a3"
},
"payload": {
"id": "513edcc1-4cbc-4274-aa26-c195f7e8c090",
"content": "oi",
"username": "hpK9jN2iY-I2HioHWr5ml",
"createdAt": "2025-11-18T22:44:29Z"
}
}
]
```
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### broadcast (binary frame)
See the [User Broadcast](#user-broadcast) section for the binary frame structure.
This message is a streamlined version of the text frame broadcast event that also supports non-JSON payloads.
Below is the same example from the previous section, showing the binary frame structure with hexadecimal values for the header and plain text for the remaining fields:
- Topic: `realtime:chat-room`
- Payload encoding being JSON
- Metadata: `{"id":"006554ce-d22d-469c-877a-88bef47214a3"}`
- User Event: `message`
- User Payload
```
0x04 // Type
0x12 // Topic Size
0x07 // User Event Size
0x2D // Metadata Size
0x01 // Payload Encoding (1 = JSON)
realtime:chat-room // Topic
message // User Event
{"id":"006554ce-d22d-469c-877a-88bef47214a3"} // Metadata
{ // User Event Payload
"id": "513edcc1-4cbc-4274-aa26-c195f7e8c090",
"content": "oi",
"username": "hpK9jN2iY-I2HioHWr5ml",
"createdAt": "2025-11-18T22:44:29Z"
}
```
The metadata field is JSON encoded. The payload encoding is just a hint for the client to know if the payload should be treated as JSON or not.
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### postgres_changes
The server sends this message when a database change occurs in a subscribed schema and table. The payload contains the details of the change, including the schema, table, event type, and the new and old records.
```ts
{
"ids": [
number
],
"data": {
"schema": string,
"table": string,
"commit_timestamp": string,
"type": "*" | "INSERT" | "UPDATE" | "DELETE",
"columns": [
{
"name": string,
"type": string
}
]
"record": {
[key: string]: boolean | number | string | null
},
"old_record": {
[key: string]: boolean | number | string | null
},
"errors": string | null
}
}
```
- `ids`: An array of unique identifiers matching the subscription when joining the channel.
- `data`: An object containing the details of the change:
- `schema`: The schema of the table where the change occurred.
- `table`: The table where the change occurred.
- `commit_timestamp`: The timestamp when the change was committed to the database.
- `type`: The type of event that occurred, such as `INSERT`, `UPDATE`, `DELETE`, or `*` for all events.
- `columns`: An array of objects representing the columns of the table, each containing:
- `name`: The name of the column.
- `type`: The data type of the column.
- `record`: An object representing the new values after the change, with keys as column names and values as their corresponding values.
- `old_record`: An object representing the old values before the change, with keys as column names and values as their corresponding values.
- `errors`: Any errors that occurred during the change, if applicable.
```json
[
null,
null,
"realtime:chat-room",
"postgres_changes",
{
"ids": [104868189],
"data": {
"schema": "public",
"table": "test",
"commit_timestamp": "2025-11-19T00:22:40.877Z",
"type": "UPDATE",
"columns": [
{
"name": "id",
"type": "int8"
},
{
"name": "created_at",
"type": "timestamptz"
},
{
"name": "text",
"type": "text"
}
],
"record": {
"id": 46,
"text": "content",
"created_at": "2025-11-03T09:32:55+00:00"
},
"old_record": {
"id": 46
},
"errors": null
}
}
]
```
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### presence_state
After joining, the server sends a `presence_state` message to a client with presence information. The payload field contains keys, where each key represents a client and its value is a JSON object containing information about that client. The key is defined by the client when joining the channel. If not specified, a UUID is automatically generated.
```ts
{
[key: string]: {
metas: [
{
phx_ref: string,
[key: string]: any
}
]
}
}
```
- `key`: The client key.
- `metas`: An array of metadata objects for the client, each containing:
- `phx_ref`: A unique reference ID for the metadata.
- Any other custom fields defined by the client, such as `name`.
Example on protocol version `2.0.0`:
```json
[
"4",
null,
"realtime:cursor-room",
"presence_state",
{
"2wCojG1xWgxG2ZxwocvSX": {
"metas": [
{
"phx_ref": "GHlA1fShRjMmZhnL",
"color": "hsl(204, 100%, 70%)",
"key": "2wCojG1xWgxG2ZxwocvSX"
}
]
},
"6eorYR7andHiq-7tCkmxQ": {
"metas": [
{
"phx_ref": "GHk99Q_ez6-GzaeG",
"color": "hsl(7, 100%, 70%)",
"key": "6eorYR7andHiq-7tCkmxQ"
}
]
},
"FOeQUamq3OLOWAAZK8iH3": {
"metas": [
{
"phx_ref": "GHk-wA8Z61GGzeoG",
"color": "hsl(212, 100%, 70%)",
"key": "FOeQUamq3OLOWAAZK8iH3"
}
]
}
}
]
```
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
#### presence_diff
After a change to the presence state, such as a client joining or leaving, the server sends a presence_diff message to update the client's view of the presence state. The payload field contains two keys, `joins` and `leaves`, which represent clients that have joined and left, respectively. Each key is either specified by the client when joining the channel or automatically generated as a UUID.
```ts
{
"joins": {
[key: string]: {
metas: [
{
phx_ref: string,
[key: string]: any
}
]
}
},
"leaves": {
[key: string]: {
metas: [
{
phx_ref: string,
[key: string]: any
}
]
}
}
}
```
- `joins`: An object containing metadata for clients that have joined the channel, with keys as UUIDs and values as metadata objects.
- `leaves`: An object containing metadata for clients that have left the channel, with keys as UUIDs and values as metadata objects.
Example on protocol version `2.0.0`:
```json
[
null,
null,
"realtime:cursor-room",
"presence_diff",
{
"joins": {
"XnAJXkZVEJuBYZcp9GCG5": {
"metas": [
{
"phx_ref": "GHlE8VLvxuKGzQJN",
"color": "hsl(60, 100%, 70%)",
"user": "123"
}
]
}
},
"leaves": {
"ouCsaiOdKZ9yauoy4x5pv": {
"metas": [
{
"phx_ref": "GHlE8HyhSPAmZgdB",
"color": "hsl(72, 100%, 70%)",
"user": "456"
}
]
}
}
}
]
```