mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 21:29:08 +03:00
Contributes to DOCS-1052 ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Resolves MDX linting errors related to "simple" where it applies. There was a couple cases that did not apply. For example, a product with "Simple" in the name. These changes are made in context, either by removing or using a more descriptive synonym like "minimal" or "basic". ## Tophatting 1. Read each of the diffs. 2. See that the text still makes sense in context. For extra due diligence, you can run `pnpm lint:mdx` locally and see the 'simple' errors that remain and whether they are worth addressing. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **Documentation** * Updated many guide, tutorial, and troubleshooting pages with clearer “basic”/“minimal” wording across setup steps, local testing instructions, security cautions, and RLS guidance. * Refined headings, example descriptions, and inline comments for consistency (including deployment, MCP, metrics API, and search/function phrasing). * Improved readability with small snippet formatting tweaks (whitespace plus import/comment ordering) and added a self-hosting debugging note for Envoy admin endpoints via a short-lived `curl` container. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Chris Chinchilla <chris.ward@supabase.io> Co-authored-by: Nik Richers <nrichers@gmail.com>
988 lines
35 KiB
Plaintext
988 lines
35 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 minimal. 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 the channel process terminates unexpectedly. Payload will be an empty object. See [Reconnection](#reconnection) for recovery guidance.
|
||
|
||
```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. When a join is rejected, `status` is `"error"` — see [Join errors](#join-errors) for the full list of error codes and recovery actions.
|
||
|
||
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. See [Channel-level system errors](#channel-level-system-errors) for the full list of messages and recovery actions.
|
||
|
||
```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"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
}
|
||
]
|
||
```
|
||
|
||
## Error handling
|
||
|
||
Errors arrive on four channels:
|
||
|
||
- A WebSocket close frame before the channel joins.
|
||
- A `phx_reply` with `status: "error"` rejecting a `phx_join` or push.
|
||
- A `system` event on a live channel — channel-level system errors are always followed by `phx_close`, while `postgres_changes` system errors are informational and leave the channel open.
|
||
- A `phx_error` when the channel process terminates unexpectedly.
|
||
|
||
{/* supa-mdx-lint-disable-next-line Rule001HeadingCase */}
|
||
|
||
### Join errors
|
||
|
||
When a `phx_join` is rejected, the `phx_reply` payload carries `response.reason` as `"<ErrorCode>: <human message>"`. The server adds a backoff delay before replying, so avoid aggressive client-side retry loops on join errors.
|
||
|
||
```json
|
||
{
|
||
"status": "error",
|
||
"response": { "reason": "InvalidJWTExpiration: Token has expired 300 seconds ago" }
|
||
}
|
||
```
|
||
|
||
One exception: the `UnknownErrorOnChannel` code arrives as the bare human-readable string `"Unknown Error on Channel"` without the `<Code>: <message>` prefix. The JS client exposes the full `reason` string directly as the Error message without parsing it further.
|
||
|
||
| Category | Error codes | Action |
|
||
| -------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------- |
|
||
| Auth — expired token | `InvalidJWTExpiration` (message contains `"expired"`) | Refresh token, rejoin |
|
||
| Auth — invalid token | `MalformedJWT`, `JwtSignatureError`, `Unauthorized` | Do not retry; surface to caller |
|
||
| Rate limit | `ConnectionRateLimitReached`, `ClientJoinRateLimitReached`, `ChannelRateLimitReached` | Backoff, reduce join frequency |
|
||
| Database | `InitializingProjectConnection`, `IncreaseConnectionPool`, `DatabaseLackOfConnections`, `UnableToConnectToProject` | Retry with exponential backoff |
|
||
| Config | `TopicNameRequired`, `TenantNotFound`, `RealtimeDisabledForTenant`, `RealtimeDisabledForConfiguration` | Do not retry |
|
||
| Transient | `RealtimeRestarting` | Retry with backoff |
|
||
|
||
### Channel-level system errors
|
||
|
||
`extension: "system"`, `status: "error"`. Match on the `message` field content — there is no machine-readable code field. Every channel-level system error is immediately followed by `phx_close`; the channel is closed. Client libraries should expose a way for users to subscribe to `system` events since there is no automatic handling.
|
||
|
||
| Message contains | Cause | Recovery |
|
||
| ------------------------------------------------- | -------------------------- | ----------------------------- |
|
||
| `Too many messages per second` | Broadcast/event rate limit | Throttle sends before rejoin |
|
||
| `Too many presence messages per second` | Tenant presence rate limit | Reduce presence frequency |
|
||
| `Client presence rate limit exceeded` | Per-client presence window | Longer cooldown before rejoin |
|
||
| `Track message size exceeded` | Presence payload too large | Shrink payload |
|
||
| `Token has expired` | JWT expired mid-session | Refresh token, rejoin |
|
||
| `Fields \`role\` and \`exp\` are required in JWT` | Claims missing | Fix token issuance |
|
||
| `Server requested disconnect` | Operational disconnect | Reconnect after delay |
|
||
|
||
### Postgres Changes subscription errors
|
||
|
||
`extension: "postgres_changes"`. These do **not** close the channel — broadcast and presence continue. `status: "ok"` with `message: "Subscribed to PostgreSQL"` confirms the subscription is live.
|
||
|
||
| Scenario | Server retries? | Client action |
|
||
| ------------------------------------------------------ | ----------------- | -------------------------------------------------------------------------- |
|
||
| Invalid filter operator | No | Fix params and rejoin |
|
||
| Missing `schema`/`table` params | No | Fix params and rejoin |
|
||
| Subscription insert failed (table/publication missing) | Yes, every 5–10 s | Surface as degraded state; wait or check Realtime is enabled for the table |
|
||
| Database error during subscription | Yes, every 5–10 s | Surface as degraded state |
|
||
| `"Too many database timeouts"` | No | Reduce subscription load; retry later |
|
||
|
||
Supported filter operators: `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `in`.
|
||
|
||
The `ids` array on incoming `postgres_changes` payloads must match the subscription IDs returned in the `phx_join` reply. A mismatch means inconsistent server/client state — tear down and rejoin.
|
||
|
||
### Broadcast errors
|
||
|
||
Broadcast errors only affect **private channels**. When `config.broadcast.ack` is `false` (the default), all push failures — including size violations and RLS write denials — are silently dropped. RLS denials are always silent regardless of `ack`.
|
||
|
||
When `ack` is `true`, the server replies on error with `response.error` (an atom string), not `response.reason`:
|
||
|
||
```json
|
||
{ "status": "error", "response": { "error": "payload_size_exceeded" } }
|
||
```
|
||
|
||
Note that the JS client (`send()`) resolves to just the string `'error'` and does not expose the specific `error` atom to callers.
|
||
|
||
### Presence errors
|
||
|
||
Push replies surface payload-shape errors with `reason: "Presence track payload must be a map"`. Other push-level failures (RLS write denied, unknown event type, internal errors) return `status: "error"` with no reason field.
|
||
|
||
Presence rate-limit and size violations arrive as channel-level system errors (see above) and close the channel.
|
||
|
||
### Access token refresh
|
||
|
||
Refresh the JWT in-band on private channels without rejoining using the `access_token` event:
|
||
|
||
```json
|
||
["10", "1", "realtime:my-channel", "access_token", { "access_token": "<new-token>" }]
|
||
```
|
||
|
||
There is no reply on success. On failure, the server emits a `system` error and closes the channel. Tokens with the `sb_*` prefix are silently ignored by the server.
|
||
|
||
### Reconnection
|
||
|
||
`phx_error` (unexpected server-side channel process termination, empty payload) should trigger a rejoin with exponential backoff. The JS client uses `[1000, 2000, 5000, 10000]` ms (capped at 10 s) configurable via `reconnectAfterMs`.
|
||
|
||
`phx_close` following a rate-limit system error requires throttling before rejoin. Following a token system error, refresh the token first. A `phx_close` with no preceding system error is a clean close — only rejoin if it was unexpected. See [Limits](/docs/guides/realtime/limits) for the per-tenant thresholds that trigger rate-limit errors.
|