Skip to content
Realtime

实时协议

WebSocket 连接建立 #

🌐 WebSocket connection setup

要开始连接,我们使用 WebSocket URL,对于:

🌐 To start the connection we use the WebSocket URL, which for:

  • Supabase 项目:wss://<PROJECT_REF>.supabase.co/realtime/v1/websocket?apikey=<API_KEY>
  • 自托管项目:wss://<HOST>:<PORT>/socket/websocket?apikey=<API_KEY>

举个例子,使用 websocat 时,你可以在终端中运行以下命令:

🌐 As an example, using websocat, you would run the following command in your terminal:

1
# With Supabase
2
websocat "wss://<PROJECT_REF>.supabase.co/realtime/v1/websocket?apikey=<API_KEY>"
3
4
# With self-hosted
5
websocat "wss://<HOST>:<PORT>/socket/websocket?apikey=<API_KEY>"

在这个阶段,你也可以设置其他 URL 参数:

🌐 During this stage you can also set other URL params:

  • vsn:设置协议版本。可能的值是 1.0.02.0.0。默认是 1.0.0
  • log_level:设置这个连接要使用的日志级别,帮助你调试潜在问题。这只会影响服务器端日志。

连接后,必须向服务器发送一个 phx_join 事件以加入通道。接下来的部分将介绍支持的不同消息类型和事件。

🌐 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

消息可以以不同的格式进行序列化。实时协议支持两个版本:1.0.02.0.0

🌐 Messages can be serialized in different formats. The Realtime protocol supports two versions: 1.0.0 and 2.0.0.

1.0.0#

版本 1.0.0 是最简化的。它使用 JSON 作为消息的序列化格式。底层的 WebSocket 消息都是文本帧。

🌐 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:正在发送或接收的事件类型。例如 phx_joinpostgres_changesbroadcast 等。
  • topic:消息所属的话题。这个字符串用来标识消息的通道或上下文。
  • payload:与事件相关的数据。这可以是任何可 JSON 序列化的数据结构,例如对象或数组。
  • ref:消息的唯一参考 ID。这对于追踪对特定消息的回复很有用。
  • join_ref:一个唯一的参考 ID,用来唯一标识已加入的主题,用于推送、广播、回复等。

示例:

🌐 Example:

1
{
2
"topic": "realtime:presence-room",
3
"event": "phx_join",
4
"payload": {
5
"config": {
6
"broadcast": {
7
"ack": false,
8
"self": false
9
},
10
"presence": {
11
"enabled": false
12
},
13
"private": false
14
}
15
},
16
"ref": "1",
17
"join_ref": "1"
18
}

2.0.0#

版本 2.0.0 使用文本和二进制 WebSocket 帧。

🌐 Version 2.0.0 uses text and binary WebSocket frames.

文字框 #

🌐 Text frames

文本框架总是采用 JSON 编码,但与 1.0.0 版本不同,它们使用一个 JSON 数组,数组元素的顺序必须完全一样:

🌐 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:

1
[
2
"1",
3
"1",
4
"realtime:presence-room",
5
"phx_join",
6
{
7
"config": {
8
"broadcast": {
9
"ack": false,
10
"self": false
11
},
12
"presence": {
13
"enabled": false
14
},
15
"private": false
16
}
17
}
18
]

二进制帧 #

🌐 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 and server sent events for more details.

代码类型描述
3USER_BROADCAST_PUSH用户发起的广播推送
4USER_BROADCAST用户广播消息

用户广播推送 #

🌐 User Broadcast Push

1
0 1 2 3
2
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
3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
4
| Type (0x03) | Join Ref Size | Ref Size | Topic Size |
5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
6
|User Event Size| Metadata Size | Payload Enc. | Join Ref ... |
7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
8
| Ref (variable length) |
9
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
10
| Topic (variable length) |
11
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
12
| User Event (variable length) |
13
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
14
| Metadata (variable length) |
15
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
16
| User Payload (variable length) |
17
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

字段描述:

  • 类型:1 字节,值 = 0x03
  • 加入引用大小:1字节,加入引用字符串的大小(最大255)
  • Ref 大小:1 字节,引用字符串的大小(最大 255)
  • 主题大小:1 字节,主题字符串的大小(最大 255)
  • 用户事件大小:1字节,用户事件字符串的大小(最大255)
  • 元数据大小:1 字节,元数据字符串的大小(最大 255)
  • 有效载荷编码:1 字节(0 = 二进制, 1 = JSON)
  • 加入参考:可变长度字符串
  • 参考:可变长度字符串
  • 主题:可变长度字符串
  • 用户事件:可变长度字符串
  • 元数据: 可变长度的 JSON 字符串
  • 用户负载:可变长度的负载数据

用户广播 #

🌐 User Broadcast

1
0 1 2 3
2
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
3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
4
| Type (0x04) | Topic Size |User Event Size| Metadata Size |
5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
6
| Payload Enc. | Topic (variable length) |
7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
8
| User Event (variable length) |
9
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
10
| Metadata (variable length) |
11
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
12
| User Payload (variable length) |
13
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

字段描述:

  • 类型:1 字节,值 = 0x04
  • 主题大小:1 字节,主题字符串的大小(最大 255)
  • 用户事件大小:1字节,用户事件字符串的大小(最大255)
  • 元数据大小:1 字节,元数据 JSON 字符串的大小(最大 255)
  • 有效载荷编码:1 字节(0 = 二进制, 1 = JSON)
  • 主题:可变长度字符串
  • 用户事件:可变长度字符串
  • 元数据: 可变长度的 JSON 字符串
  • 用户负载:可变长度的负载数据

活动类型 #

🌐 Event types

除了 broadcast 事件类型可以出现在文本帧和二进制帧上,所有事件的消息都以 JSON 格式的文本帧编码。

🌐 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

事件类型描述需要 Ref需要 Join Ref
phx_join初始消息,用于加入通道并配置功能
phx_leave离开通道的消息
heartbeat心跳消息,用于保持连接
access_token更新访问令牌的消息
broadcast发送给通道中所有客户端的广播消息
presence加入通道后发送的在线状态更新

phx_join#

这是加入通道所需的初始消息。客户端发送此消息到服务器,以加入特定的主题并配置它想要使用的功能,比如 Postgres 变更、在线状态和广播。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.

1
{
2
"config": {
3
"broadcast": {
4
"ack": boolean,
5
"self": boolean,
6
"replay" : {
7
"since": integer,
8
"limit": integer
9
},
10
"replication_ready": boolean
11
},
12
"presence": {
13
"enabled": boolean,
14
"key": string
15
},
16
"postgres_changes": [
17
{
18
"event": string,
19
"schema": string,
20
"table": string,
21
"filter": string,
22
"select": string[]
23
}
24
]
25
"private": boolean
26
},
27
"access_token": string
28
}
  • config:
    • private:这个通道是否是私有的
    • broadcast:广播消息的配置选项
      • ack:确认广播消息
      • self:在群发消息中包括发送者
      • replay:广播回放的配置选项(可选)
        • since:从特定时间戳(毫秒)重放消息
        • limit:限制重放消息的数量(可选)
      • replication_ready:当 true 时,一旦支持该通道的 Postgres 复制连接建立并准备好传输更改,服务器会触发一个 system 事件(可选)。有关负载格式,请查看 system 事件。
    • presence:出席跟踪的配置选项
      • enabled:这个通道是否启用了在线状态追踪
      • key:用于存在跟踪的钥匙,如果未指定或为空,将生成并使用一个 UUID
    • postgres_changes:Postgres 更改的配置数组
      • event:要监听的数据库变更事件,可接受 INSERTUPDATEDELETE* 来监听所有事件。
      • schema:要监听的表的模式,接受 * 通配符来监听所有模式
      • table:要监听的数据库表,接受 * 通配符来监听所有表
      • filter:从数据库提取更改时使用的过滤器。过滤器是一个 column=operator.value 表达式(例如 id=eq.1title=like.%foo%)。可以用逗号组合多个条件,并作为 AND 应用(例如 id=gt.0,id=lt.100)。任何操作符都可以用 not. 前缀否定(例如 status=not.in.(draft,archived))。值中的保留字符(,())必须使用 PostgREST 风格的双引号括起来(例如 name=eq."a,b")。有关支持的操作符完整列表,请参见 Postgres Changes 订阅错误,以及 Postgres Changes 的使用文档。
      • select:可选的列名数组,用于将变更负载限制为部分列,而不是接收整行数据。这样可以减少每次事件传输的数据量。列名必须是订阅角色可选择的。通配符(*)模式或表订阅不支持,需要明确指定 schematable
  • access_token:可选的访问令牌用于身份验证,如果不提供,服务器将使用 API 密钥。

关于协议版本 2.0.0 的示例:

🌐 Example on protocol version 2.0.0:

1
[
2
"3",
3
"5",
4
"realtime:chat-room",
5
"phx_join",
6
{
7
"config": {
8
"broadcast": {
9
"ack": false,
10
"self": true,
11
"replay": {
12
"since": 1763407103911,
13
"limit": 10
14
}
15
},
16
"presence": {
17
"key": "user_id-827",
18
"enabled": true
19
},
20
"postgres_changes": [],
21
"private": true
22
}
23
}
24
]

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.

关于协议版本 2.0.0 的示例:

🌐 Example on protocol version 2.0.0:

1
["1", "3", "realtime:avatar-stack-demo", "phx_leave", {}]

heartbeat#

心跳消息应该至少每25秒发送一次,以避免连接超时。负载应为空对象。

🌐 The heartbeat message should be sent at least every 25 seconds to avoid a connection timeout. Payload should be an empty object.

对于心跳,使用主题 phoenix,因为这个特殊消息不连接到特定通道。

🌐 For heartbeat, the topic phoenix is used as this special message is not connected to a specific channel.

关于协议版本 2.0.0 的示例:

🌐 Example on protocol version 2.0.0:

1
[null, "26", "phoenix", "heartbeat", {}]

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.

1
{
2
"access_token": string
3
}
  • access_token:用于身份验证的新访问令牌。可以用它来更改或刷新令牌。

关于协议版本 2.0.0 的示例:

🌐 Example on protocol version 2.0.0:

1
[
2
"10",
3
"1",
4
"realtime:chat-room",
5
"access_token",
6
{
7
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
8
}
9
]

广播(文本框架) #

🌐 broadcast (text frame)

用来向通道里的所有客户端发送广播事件。

🌐 Used to send a broadcast event to all clients in a channel.

payload 字段包含要广播的事件名称和数据。

🌐 The payload field contains the event name and the data to broadcast.

1
{
2
"event": string,
3
"payload": json,
4
"type": "broadcast"
5
}
  • event:要广播的用户事件名称。
  • payload:与事件相关的用户数据,可以是任何可 JSON 序列化的数据结构。
  • type:消息的类型,必须始终是 broadcast

关于协议版本 2.0.0 的示例:

🌐 Example on protocol version 2.0.0:

1
[
2
"10",
3
"1",
4
"realtime:chat-room",
5
"broadcast",
6
{
7
"event": "user-event",
8
"type": "broadcast",
9
"payload": {
10
"content": "Hello, World!",
11
"createdAt": "2025-11-17T21:14:14Z",
12
"id": "9b823349-71c0-465b-9a83-a63aa2a9ae6d",
13
"username": "VCSHLD556nQD-B-vUTJJ3"
14
}
15
}
16
]

广播(二进制帧) #

🌐 broadcast (binary frame)

请查看 用户广播推送 部分了解二进制帧结构。

🌐 See the User Broadcast Push section for the binary frame structure.

此消息是文本帧广播事件的简化版本,也支持非 JSON 负载。 下面是上一节的相同示例,展示了带有十六进制头部值和其余字段为纯文本的二进制帧结构:

🌐 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:

  • 加入参考:10
  • 参考:1
  • 话题:realtime:chat-room
  • 有效载荷编码为 JSON
  • 用户事件:user-event
  • 元数据为空
  • 用户负载
1
0x03 // Type
2
0x02 // Join Ref Size
3
0x01 // Ref Size
4
0x12 // Topic Size
5
0x0A // User Event Size
6
0x00 // Metadata Size
7
0x01 // Payload Encoding (1 = JSON)
8
10 // Actual Join Ref
9
1 // Actual Ref
10
realtime:chat-room // Topic
11
user-event // User Event
12
{ // User Event Payload
13
"content": "Hello, World!",
14
"createdAt": "2025-11-17T21:14:14Z",
15
"id": "9b823349-71c0-465b-9a83-a63aa2a9ae6d",
16
"username": "VCSHLD556nQD-B-vUTJJ3"
17
}

负载编码是给客户端的一个提示,让它知道负载是否应该被当作 JSON 处理。

🌐 The payload encoding is a hint for the client to know if the payload should be treated as JSON or not.

presence#

用来在加入通道后发送在线状态元数据。负载包含服务器需要跟踪的在线信息。 这些元数据随后会通过 presence_statepresence_diff 事件发回通道里的所有客户端。

🌐 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.

1
{
2
"type": "presence",
3
"event": "track",
4
"payload": json
5
}

关于协议版本 2.0.0 的示例:

🌐 Example on protocol version 2.0.0:

1
[
2
"1",
3
"5",
4
"realtime:presence-room",
5
"presence",
6
{
7
"type": "presence",
8
"event": "track",
9
"payload": {
10
"name": "Alice",
11
"color": "hsl(29, 100%, 70%)"
12
}
13
}
14
]

服务器发送事件 #

🌐 Server sent events

事件类型描述需要引用需要加入引用
phx_close服务器发送的消息,用来表示通道已关闭
phx_error服务器在发生错误时发送的错误消息
phx_replyphx_join 或其他请求的响应✅*
system系统消息,用来告知 Postgres 订阅的状态
broadcast发送给通道中所有客户端的广播消息
presence_state服务器在加入时发送的在线状态
presence_diff在线状态变化后发送的状态差异更新
postgres_changes包含数据库变更的 Postgres CDC 消息

phx_close#

这条消息由服务器发送,用来表示通道已关闭。负载将是空对象。

🌐 This message is sent by the server to signal that the channel has been closed. Payload will be empty object.

关于协议版本 2.0.0 的示例:

🌐 Example on protocol version 2.0.0:

1
["3", "3", "realtime:avatar-stack-demo", "phx_close", {}]

phx_error#

当通道进程意外终止时,服务器会发送此消息。有效负载将是一个空对象。有关恢复的指导,请参阅重新连接

🌐 This message is sent by the server when the channel process terminates unexpectedly. Payload will be an empty object. See Reconnection for recovery guidance.

1
["3", "3", "realtime:avatar-stack-demo", "phx_error", {}]

phx_reply#

服务器会发送这些消息,以回应需要确认的客户端请求。

🌐 The server sends these messages in response to client requests that require acknowledgment.

1
{
2
"status": string,
3
"response": any,
4
}
  • status:响应的状态可以是 okerror
  • response:响应数据,它可能会根据所回复的事件而有所不同

phx_join 有一个特定的响应结构,如下所示。当加入被拒绝时,status"error"——完整的错误代码和恢复操作请参见 Join errors

包含加入请求的状态以及 phx_join 负载中请求的任何附加信息。

🌐 Contains the status of the join request and any additional information requested in the phx_join payload.

1
{
2
"postgres_changes": [
3
{
4
"id": number,
5
"event": string,
6
"schema": string,
7
"table": string
8
}
9
]
10
}
  • postgres_changes:客户端订阅的 Postgres 变更数组,每个对象包含:
    • id:Postgres 变更订阅的唯一标识符
    • event:客户端订阅的事件类型,比如 INSERTUPDATEDELETE*
    • schema:客户端订阅的表的模式
    • table:客户订阅的表格

关于协议版本 2.0.0 的示例:

🌐 Example on protocol version 2.0.0:

1
[
2
"1",
3
"1",
4
"realtime:chat-room",
5
"phx_reply",
6
{
7
"status": "ok",
8
"response": {
9
"postgres_changes": [
10
{
11
"id": 106243155,
12
"event": "*",
13
"schema": "public",
14
"table": "test"
15
}
16
]
17
}
18
}
19
]

system#

服务器会发送系统消息,通知客户端他们的实时通道订阅状态。完整的消息列表和恢复操作请参见 通道级别系统错误

🌐 The server sends system messages to inform clients about the status of their Realtime channel subscriptions. See Channel-level system errors for the full list of messages and recovery actions.

1
{
2
"message": string,
3
"status": string,
4
"extension": string,
5
"channel": string
6
}
  • message:用人类能理解的方式描述订阅状态的信息。
  • status:订阅的状态可以是 okerrortimeout
  • extension:发送消息的扩展。postgres_changes 用于 Postgres 更改订阅状态,或者 system 用于连接级别的消息,例如复制就绪通知。
  • channel:消息所属的通道,比如 realtime:room1

关于协议版本 2.0.0 的示例:

🌐 Example on protocol version 2.0.0:

1
[
2
"13",
3
null,
4
"realtime:chat-room",
5
"system",
6
{
7
"message": "Subscribed to PostgreSQL",
8
"status": "ok",
9
"extension": "postgres_changes",
10
"channel": "main"
11
}
12
]

当一个通道加入时,如果 config.broadcast.replication_ready 设置为 true,一旦支持该通道的 Postgres 复制连接准备好传输变更,服务器就会发送带有 extension: "system"system 消息。status 在成功时是带有 message: "Replication connection established""ok",如果连接未及时建立则是 "error"(同时也会关闭通道——参见 通道级系统错误)。

🌐 When a channel is joined with config.broadcast.replication_ready set to true, the server sends a system message with extension: "system" once the Postgres replication connection backing the channel is ready to stream changes. status is "ok" with message: "Replication connection established" on success, or "error" if the connection is not established in time (which also closes the channel — see Channel-level system errors).

1
[
2
"14",
3
null,
4
"realtime:chat-room",
5
"system",
6
{
7
"message": "Replication connection established",
8
"status": "ok",
9
"extension": "system",
10
"channel": "main"
11
}
12
]

广播(文本框架) #

🌐 broadcast (text frame)

这是所有订阅某个通道的客户端接收到的广播事件的结构。payload 字段包含被广播的事件名称和数据。

🌐 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.

1
{
2
"event": string,
3
"meta" : {
4
"id" : uuid,
5
"replayed" : boolean
6
},
7
"payload": json,
8
"type": "broadcast"
9
}
  • event:要广播的用户事件名称。
  • meta:关于广播消息的元数据。并不总是存在。
    • id:以 UUID 格式表示的广播消息唯一标识符。
    • replayed:一个布尔值,表示该消息是否为重放消息。并不总是存在
  • payload:与事件相关的用户数据,可以是任何可 JSON 序列化的数据结构。
  • type:消息类型,对于广播消息必须始终是 broadcast

关于协议版本 2.0.0 的示例:

🌐 Example on protocol version 2.0.0:

1
[
2
null,
3
null,
4
"realtime:chat-room",
5
"broadcast",
6
{
7
"event": "message",
8
"type": "broadcast",
9
"meta": {
10
"id": "006554ce-d22d-469c-877a-88bef47214a3"
11
},
12
"payload": {
13
"id": "513edcc1-4cbc-4274-aa26-c195f7e8c090",
14
"content": "oi",
15
"username": "hpK9jN2iY-I2HioHWr5ml",
16
"createdAt": "2025-11-18T22:44:29Z"
17
}
18
}
19
]

广播(二进制帧) #

🌐 broadcast (binary frame)

查看 用户广播 部分了解二进制帧结构。

🌐 See the User Broadcast section for the binary frame structure.

此消息是文本帧广播事件的简化版本,也支持非 JSON 负载。 下面是上一节的相同示例,展示了带有十六进制头部值和其余字段为纯文本的二进制帧结构:

🌐 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:

  • 话题:realtime:chat-room
  • 有效载荷编码为 JSON
  • 元数据:{"id":"006554ce-d22d-469c-877a-88bef47214a3"}
  • 用户事件:message
  • 用户负载
1
0x04 // Type
2
0x12 // Topic Size
3
0x07 // User Event Size
4
0x2D // Metadata Size
5
0x01 // Payload Encoding (1 = JSON)
6
realtime:chat-room // Topic
7
message // User Event
8
{"id":"006554ce-d22d-469c-877a-88bef47214a3"} // Metadata
9
{ // User Event Payload
10
"id": "513edcc1-4cbc-4274-aa26-c195f7e8c090",
11
"content": "oi",
12
"username": "hpK9jN2iY-I2HioHWr5ml",
13
"createdAt": "2025-11-18T22:44:29Z"
14
}

元数据字段是 JSON 编码的。有效负载编码是给客户端的一个提示,让它知道是否应该将有效负载当作 JSON 处理。

🌐 The metadata field is JSON encoded. The payload encoding is a hint for the client to know if the payload should be treated as JSON or not.

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.

1
{
2
"ids": [
3
number
4
],
5
"data": {
6
"schema": string,
7
"table": string,
8
"commit_timestamp": string,
9
"type": "*" | "INSERT" | "UPDATE" | "DELETE",
10
"columns": [
11
{
12
"name": string,
13
"type": string
14
}
15
]
16
"record": {
17
[key: string]: boolean | number | string | null
18
},
19
"old_record": {
20
[key: string]: boolean | number | string | null
21
},
22
"errors": string | null
23
}
24
}
  • ids:一组在加入通道时与订阅匹配的唯一标识符。
  • data:包含更改详情的对象:
    • schema:发生更改的表的架构。
    • table:发生变化的那张桌子。
    • commit_timestamp:更改提交到数据库的时间戳。
    • type:发生的事件类型,例如所有事件的 INSERTUPDATEDELETE*
    • columns:一个表示表格列的对象数组,每个对象包含:
      • name:这一列的名字。
      • type:这一列的数据类型。
    • record:一个对象,表示更改后的新值,键是列名,值是对应的值。
    • old_record:一个对象,表示更改前的旧值,键是列名,值是对应的值。
    • errors:在更改过程中发生的任何错误(如果有的话)。

当订阅与一个 select 数组(参见 phx_join)一起加入时,columnsrecordold_record 会被限制为选定的列,而不是整行。

🌐 When the subscription was joined with a select array (see phx_join), columns, record, and old_record are restricted to the selected columns instead of the full row.

1
[
2
null,
3
null,
4
"realtime:chat-room",
5
"postgres_changes",
6
{
7
"ids": [104868189],
8
"data": {
9
"schema": "public",
10
"table": "test",
11
"commit_timestamp": "2025-11-19T00:22:40.877Z",
12
"type": "UPDATE",
13
"columns": [
14
{
15
"name": "id",
16
"type": "int8"
17
},
18
{
19
"name": "created_at",
20
"type": "timestamptz"
21
},
22
{
23
"name": "text",
24
"type": "text"
25
}
26
],
27
"record": {
28
"id": 46,
29
"text": "content",
30
"created_at": "2025-11-03T09:32:55+00:00"
31
},
32
"old_record": {
33
"id": 46
34
},
35
"errors": null
36
}
37
}
38
]

presence_state#

加入后,服务器会向客户端发送一个 presence_state 消息,里面带有在线状态信息。payload 字段包含一些键,每个键代表一个客户端,其对应的值是一个包含该客户端信息的 JSON 对象。这个键是在客户端加入通道时定义的,如果没指定,系统会自动生成一个 UUID。

🌐 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.

1
{
2
[key: string]: {
3
metas: [
4
{
5
phx_ref: string,
6
[key: string]: any
7
}
8
]
9
}
10
}
  • key:客户端密钥。
  • metas:客户端的一组元数据对象,每个对象包含:
    • phx_ref:元数据的唯一参考ID。
    • 客户定义的任何其他自定义字段,比如 name

关于协议版本 2.0.0 的示例:

🌐 Example on protocol version 2.0.0:

1
[
2
"4",
3
null,
4
"realtime:cursor-room",
5
"presence_state",
6
{
7
"2wCojG1xWgxG2ZxwocvSX": {
8
"metas": [
9
{
10
"phx_ref": "GHlA1fShRjMmZhnL",
11
"color": "hsl(204, 100%, 70%)",
12
"key": "2wCojG1xWgxG2ZxwocvSX"
13
}
14
]
15
},
16
"6eorYR7andHiq-7tCkmxQ": {
17
"metas": [
18
{
19
"phx_ref": "GHk99Q_ez6-GzaeG",
20
"color": "hsl(7, 100%, 70%)",
21
"key": "6eorYR7andHiq-7tCkmxQ"
22
}
23
]
24
},
25
"FOeQUamq3OLOWAAZK8iH3": {
26
"metas": [
27
{
28
"phx_ref": "GHk-wA8Z61GGzeoG",
29
"color": "hsl(212, 100%, 70%)",
30
"key": "FOeQUamq3OLOWAAZK8iH3"
31
}
32
]
33
}
34
}
35
]

presence_diff#

在状态变化后,比如客户端加入或离开时,服务器会发送一个 presence_diff 消息来更新客户端的状态视图。负载字段包含两个键,joinsleaves,分别代表加入和离开的客户端。每个键要么是在加入通道时由客户端指定,要么是自动生成的 UUID。

🌐 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.

1
{
2
"joins": {
3
[key: string]: {
4
metas: [
5
{
6
phx_ref: string,
7
[key: string]: any
8
}
9
]
10
}
11
},
12
"leaves": {
13
[key: string]: {
14
metas: [
15
{
16
phx_ref: string,
17
[key: string]: any
18
}
19
]
20
}
21
}
22
}
  • joins:一个包含已加入通道的客户端元数据的对象,键是 UUID,值是元数据对象。
  • leaves:一个包含已离开通道的客户端元数据的对象,键是 UUID,值是元数据对象。

关于协议版本 2.0.0 的示例:

🌐 Example on protocol version 2.0.0:

1
[
2
null,
3
null,
4
"realtime:cursor-room",
5
"presence_diff",
6
{
7
"joins": {
8
"XnAJXkZVEJuBYZcp9GCG5": {
9
"metas": [
10
{
11
"phx_ref": "GHlE8VLvxuKGzQJN",
12
"color": "hsl(60, 100%, 70%)",
13
"user": "123"
14
}
15
]
16
}
17
},
18
"leaves": {
19
"ouCsaiOdKZ9yauoy4x5pv": {
20
"metas": [
21
{
22
"phx_ref": "GHlE8HyhSPAmZgdB",
23
"color": "hsl(72, 100%, 70%)",
24
"user": "456"
25
}
26
]
27
}
28
}
29
}
30
]

错误处理 #

🌐 Error handling

错误出现在四个通道上:

🌐 Errors arrive on four channels:

  • 在通道加入之前的 WebSocket 关闭帧。
  • 一个带有 status: "error"phx_reply 拒绝了 phx_join 或推送。
  • 在直播通道上发生 system 事件——通道级系统错误总是会跟着 phx_close,而 postgres_changes 系统错误只是提示信息,并且不会关闭通道。
  • 当通道进程意外终止时的 phx_error

连接错误 #

🌐 Join errors

phx_join 被拒绝时,phx_reply 负载会以 "<ErrorCode>: <human message>" 的形式携带 response.reason。服务器在回复前会增加一个延迟,所以在加入错误时避免过于频繁的客户端重试循环。

1
{
2
"status": "error",
3
"response": { "reason": "InvalidJWTExpiration: Token has expired 300 seconds ago" }
4
}

有一个例外:UnknownErrorOnChannel 代码会以裸露的人类可读字符串 "Unknown Error on Channel" 到达,而没有 <Code>: <message> 前缀。JS 客户端会直接将完整的 reason 字符串作为错误信息暴露出来,而不会进一步解析。

🌐 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.

类别错误代码处理措施
认证 — 令牌过期InvalidJWTExpiration(消息包含 "expired"刷新令牌,重新加入
认证 — 无效令牌MalformedJWTJwtSignatureErrorUnauthorized不要重试;通知调用方
速率限制ConnectionRateLimitReachedClientJoinRateLimitReachedChannelRateLimitReached等一等,降低加入频率
数据库InitializingProjectConnectionIncreaseConnectionPoolDatabaseLackOfConnectionsUnableToConnectToProject使用指数退避重试
配置TopicNameRequiredTenantNotFoundRealtimeDisabledForTenantRealtimeDisabledForConfiguration不要重试
临时错误RealtimeRestarting使用退避策略重试

通道级系统错误 #

🌐 Channel-level system errors

extension: "system"status: "error"。根据 message 字段内容匹配——没有可机器读取的代码字段。每次出现通道级系统错误时,紧接着都会有 phx_close;通道会被关闭。客户端库应该提供一种方式,让用户可以订阅 system 事件,因为没有自动处理机制。

消息包含原因解决方法
Too many messages per second广播/事件速率限制在重新加入前控制发送速率
Too many presence messages per second租户在线状态速率限制减少在线状态发送频率
Client presence rate limit exceeded每个客户端的在线状态窗口重新加入前延长冷却时间
Track message size exceeded在线状态负载太大缩小负载
Token has expired会话中 JWT 已过期刷新令牌并重新加入
Fields \role` and `exp` are required in JWT`缺少声明修复令牌发放
Server requested disconnect操作断开连接延迟后重新连接
Replication connection was not established in time在截止时间前复制连接未就绪(仅在请求了 replication_ready 时)采用指数退避重试

Postgres 更改订阅时出错 #

🌐 Postgres Changes subscription errors

extension: "postgres_changes"。这些不会关闭通道——广播和在线状态仍会继续。status: "ok"配合message: "Subscribed to PostgreSQL"确认订阅处于活跃状态。

场景服务器会重试吗?客户端操作
无效的过滤操作符不会修复参数并重新加入
缺少 schema/table 参数不会修复参数并重新加入
订阅插入失败(表/发布缺失)会,每 5–10 秒显示为降级状态;等待或检查表的实时功能是否已开启
订阅期间数据库错误会,每 5–10 秒显示为降级状态
"Too many database timeouts"不会降低订阅负载;稍后重试

支持的过滤器操作符:eqneqltltegtgteinlikeilikeismatchimatchisdistinct。任何操作符都可以通过 not. 前缀进行取反(例如 id=not.eq.5)。多个条件可以用逗号组合,并作为 AND 应用(例如 col1=eq.val,col2=gt.5)。

🌐 Supported filter operators: eq, neq, lt, lte, gt, gte, in, like, ilike, is, match, imatch, isdistinct. Any operator can be negated with the not. prefix (for example id=not.eq.5). Multiple conditions are combined with commas and applied as an AND (for example col1=eq.val,col2=gt.5).

传入的 postgres_changes 数据包中的 ids 数组必须与 phx_join 回复中返回的订阅 ID 一致。如果不匹配,就表示服务器/客户端状态不一致——需要断开并重新加入。

🌐 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

广播错误只会影响私有通道。当 config.broadcast.ackfalse(默认值)时,所有推送失败——包括大小违反和 RLS 写入拒绝——都会被悄悄丢弃。无论 ack 是什么,RLS 拒绝始终都是静默的。

🌐 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.

acktrue 时,服务器在出错时会回复 response.error(一个原子字符串),而不是 response.reason

🌐 When ack is true, the server replies on error with response.error (an atom string), not response.reason:

1
{ "status": "error", "response": { "error": "payload_size_exceeded" } }

注意,JS 客户端(send())解析为字符串 'error',并不会向调用方暴露具体的 error 原子。

🌐 Note that the JS client (send()) resolves to the string 'error' and does not expose the specific error atom to callers.

存在错误 #

🌐 Presence errors

推送回复会在 reason: "Presence track payload must be a map" 中显示负载形状错误。其他推送级别的失败(RLS 写入被拒绝、未知事件类型、内部错误)会返回 status: "error",但不会有原因字段。

🌐 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

在私有通道上使用 access_token 事件刷新 JWT,无需重新加入:

🌐 Refresh the JWT in-band on private channels without rejoining using the access_token event:

1
["10", "1", "realtime:my-channel", "access_token", { "access_token": "<new-token>" }]

成功时不会有回复。失败时,服务器会发出 system 错误并关闭通道。带有 sb_* 前缀的令牌会被服务器静默忽略。

🌐 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(服务器端通道进程意外终止,负载为空)应该触发带指数回退的重新加入。JS 客户端使用 [1000, 2000, 5000, 10000] 毫秒(上限为 10 秒),可以通过 reconnectAfterMs 配置。

phx_close 在遇到速率限制系统错误后,需要在重新加入前进行节流。遇到令牌系统错误时,先刷新令牌。没有前置系统错误的 phx_close 是正常关闭 —— 只有在意外情况下才重新加入。有关触发速率限制错误的每个租户阈值,请参见 Limits