# 实时订阅（Realtime）
#### 场景介绍
Supabase自带Realtime WebSocket套件，无需自行开发，它是一个全自动的实时数据同步引擎。简单来说，它就像在客户端与数据库之间架设了一条"超高速公路"，一旦数据库里的数据发生变化（如增、删、改操作），客户端不需要刷新页面，也不需要主动发请求，就能秒级收到最新数据。
Supabase Realtime让客户端与服务端维持一条WebSocket长连接，服务器有数据变化或消息时，主动推送给已连接的客户端。Supabase Realtime提供以下**三大功能**：
- **Postgres Changes**：监听数据库表的INSERT/UPDATE/DELETE，表中数据变化时通知客户端。
- **Broadcast**：客户端之间直接互发消息，适合通知、协同编辑。
- **Presence**：跟踪频道里"谁在线"。
**常见应用场景**：
- 实时数据展示：订单量、库存、监控指标随业务发生即时刷新，无需反复刷新页面。
- 协同编辑：多人同时编辑文档或白板，彼此实时看到对方改动。
- 即时通讯：消息即时收发、在线成员列表、"对方正在输入"等聊天能力。
- 实时通知：价格变动、审核结果、系统告警第一时间送达客户端。
三项功能都由@supabase/supabase-js统一管理，连接建立、断线重连由SDK自动处理，权限由服务端RLS按登录凭证判定，开发时只需关注订阅与消息接收。
![](https://support.huaweicloud.com/usermanual-rds-pg/public_sys-resources/note_3.0-zh-cn.png)
**频道（channel）**：Realtime中一个命名的消息通道。客户端订阅某频道后，只接收该频道里流转的事件。Postgres Changes、Broadcast、Presence都依托频道传递。
**Supabase Realtime的** **核心价值** **：**
- 告别手动刷新：数据变化时客户端自动收到，无需轮询或手动刷新，体验接近即时通讯应用。
- 接入门槛低：连接管理、断线重连等由SDK封装，调用subscribe()即可订阅。
- 与RLS结合的权限隔离：只推送用户有权查看的行变更，无需额外写鉴权。
对不同角色，价值各有侧重：
- 独立开发者（OPC）：能够以低成本实现专业级产品。仅需几行代码，即可为产品添加协同看板、实时图表等实时交互功能；长连接仅在数据变化时推送，从而显著降低CPU和带宽占用；数据写入Postgres后自动广播，数据库承担消息队列职责，无需额外维护实时组件。
- 快速上手的开发者：低门槛接入、即时获得反馈。前端几行代码即可订阅数据变更并实时更新页面；连接、鉴权、广播由Supabase托管，可专注于前端界面实现。
本章节以实时聊天室为案例，系统地介绍三大能力：消息存库并通过Postgres Changes实时同步；使用Broadcast传递"正在输入"等瞬时状态；以及通过Presence追踪在线成员名单。
 #### 配置启用
Supabase出于性能考虑，默认不把任何表的变更推给客户端，否则每张表的每次改动都会被推送，开销过大。因此需要先完成一步登记：告知数据库哪些表的变更允许对外发布。
登记的对象称为publication（发布），相当于数据库中的一份"发布清单"：列入清单的表，其变更才会被发出；未列入的表，Realtime不会推送。Supabase预置了名为supabase_realtime的publication，将表加入即可。
聊天室示例涉及表profiles与messages，详见[表1]。
 表1聊天室数据表 
| 表名       | 用途               | 主要字段                                                                    |
|:---|:---|:---|
| profiles   | 用户信息表，存储展示名与头像。 | id（主键，关联auth.users）、name（展示名）、avatar（头像）。                              |
| messages | 消息表，存储聊天消息。       | id（主键）、room_id（房间）、user_id（发送者，关联profiles）、content（内容）、created_at（时间）。 |
   
建表SQL：
```
-- profiles：用户信息表
CREATE TABLE profiles (
  id         UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  name       TEXT NOT NULL,
  avatar     TEXT,
  created_at TIMESTAMPTZ DEFAULT now()
);
-- messages：消息表（须在 profiles 之后创建，因其引用 profiles(id)）
CREATE TABLE messages (
  id         BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  room_id    TEXT NOT NULL,
  user_id    UUID REFERENCES profiles(id),
  content    TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);
```
权限配置（RLS）：聊天室需登录，因此只对authenticated放行，消息的修改与删除仅限发送者本人。
```
-- profiles：登录用户可读所有资料，仅可改自己
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "profiles_select_all" ON public.profiles
  FOR SELECT TO authenticated USING (true);
CREATE POLICY "profiles_update_own" ON public.profiles
  FOR UPDATE TO authenticated USING (auth.uid() = id) WITH CHECK (auth.uid() = id);
-- messages：登录用户可读所有消息，增改删限本人
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "messages_select_all" ON public.messages
  FOR SELECT TO authenticated USING (true);
CREATE POLICY "messages_insert_own" ON public.messages
  FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id);
CREATE POLICY "messages_update_own" ON public.messages
  FOR UPDATE TO authenticated USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
CREATE POLICY "messages_delete_own" ON public.messages
  FOR DELETE TO authenticated USING (auth.uid() = user_id);
```
将需要监听变更的表加入发布清单（此处监听messages，消息增删改即对外推送）：
```
ALTER PUBLICATION supabase_realtime ADD TABLE public.messages;
```
被监听的表必须有主键，由于逻辑复制通过主键定位"被改的是哪一行"，没有主键便无法定位具体行，也就无法精确推送变更。
加入清单并不意味着客户端一定能收到，中间还有一道RLS权限校验。RLS是Postgres逐行判断"当前客户端是否可以看到该行"的规则。Supabase依据客户端携带的JWT（一种登录凭证）中声明的角色判断身份，而非更换key。
表2客户端角色与可见范围 
| 客户端角色                      | 可见范围                         |
|:---|:---|
| anon（未登录，仅用公开密钥）             | 只能看到允许anon角色查看的行变更。            |
| authenticated（已登录，携带登录凭证） | 只能看到允许authenticated角色查看的行变更。    |
| service_role（使用服务端密钥）        | 所有行变更（该角色带BYPASSRLS，直接绕过规则）。 |
   
因此一条变更要送达客户端，需先通过"发布清单"（表是否已加入），再通过RLS权限校验（RLS是否放行）。
将表加入清单后，一条变更从数据库到浏览器的路径如下：表中某行被增删改时，Postgres通过原生逻辑复制能力（数据库自带的变更通知机制）将变更打包发出；Realtime服务订阅了supabase_realtime这个发布，收到变更后经WebSocket长连接转发给已连接的浏览器。因此加入publication这一步，本质是让某张表的变化进入这条推送链路，未加入则变化不进入链路，客户端无法收到。
 #### 监听数据变更（Postgres Changes）
Postgres Changes用于监听数据库表的数据变化。客户端订阅一个频道、指明要监听哪个表的什么操作（如增、改、删操作），此后该表发生对应变化时，Realtime通过WebSocket将变化数据推送给回调函数。下面以聊天室的messages表为例，介绍当有人发消息（INSERT）、编辑消息（UPDATE）、撤回消息（DELETE）时，订阅者实时感知并更新界面。
![](https://support.huaweicloud.com/usermanual-rds-pg/public_sys-resources/note_3.0-zh-cn.png)
订阅不会立即生效：调用subscribe()后，频道需先与服务器握手成功才表示真正上线（成功时status为SUBSCRIBED）。正式代码建议为subscribe()传入回调，确认SUBSCRIBED后再依赖监听，并对CHANNEL_ERROR、TIMED_OUT做处理。以下示例中省略该段，正式项目中建议补齐相应处理。
1. **监听INSERT**
   新消息即时上屏：一方在聊天室发出消息，messages表新增一行，其他在线成员无需刷新即可看到这条新消息。这需要监听messages表的INSERT，即表每新增一行，Realtime便向订阅者推送一条INSERT事件。
   执行以下代码创建一个频道，用on()声明监听public schema的messages表的INSERT，再调用subscribe()上线；此后该表每插入一条消息，回调触发一次，新消息的整行数据在payload.new里（含room_id、user_id、content等）。
   ```
   const channel = supabase
     .channel('new-messages')
     .on('postgres_changes', {
       event: 'INSERT',
       schema: 'public',
       table: 'messages'
     }, (payload) => {
       console.log('新消息:', payload.new)
     })
     .subscribe()
   ```
   
2. **监听UPDATE**
   消息编辑同步：发送者修改了自己刚发的消息（如纠正错字），所有打开该聊天室的用户应立即看到新内容；部分场景还需突出"改动了哪些字段"。这通过监听messages表的UPDATE实现，即表中某行被修改时推送UPDATE事件，回调同时拿到修改前（payload.old）和修改后（payload.new）的数据，方便对比差异。
   获取旧数据的前提：需将表的REPLICA IDENTITY（副本标识）设为FULL，否则旧值为空。Postgres默认UPDATE只记录主键、不记录旧值，开启FULL（通过**ALTER TABLE messages REPLICA IDENTITY FULL;**开启）才会一并带上整行旧数据。
   ```
   const channel = supabase
     .channel('edited-messages')
     .on('postgres_changes', {
       event: 'UPDATE',
       schema: 'public',
       table: 'messages'
     }, (payload) => {
       console.log('编辑前:', payload.old)
       console.log('编辑后:', payload.new)
     })
     .subscribe()
   ```
   
3. **监听DELETE**
   消息撤回即时移除：发送者撤回（删除）了消息，所有客户端应立刻从聊天界面移除该项，避免显示已撤回的内容。这通过监听messages表的DELETE实现，即表中某行被删除时推送DELETE事件，回调从payload.old获取被删行的数据，据此从消息列表移除。
   该操作同样需要REPLICA IDENTITY设为FULL。
   ```
   const channel = supabase
     .channel('deleted-messages')
     .on('postgres_changes', {
       event: 'DELETE',
       schema: 'public',
       table: 'messages'
     }, (payload) => {
       console.log('已撤回:', payload.old)
     })
     .subscribe()
   ```
   
4. **监听所有事件**
   有时只关心"该聊天室有动静"：用于刷新本地缓存、记录审计日志等，不区分是新消息、编辑还是撤回。此时将event设为\*，一次监听增、改、删三类。回调里用payload.eventType判断本次是INSERT、UPDATE还是DELETE，再用payload.new或payload.old取对应数据。
   代码中"payload.new \|\| payload.old"是一种实用写法：INSERT有"new"无"old"、DELETE有"old"无"new"，使用该写法无论哪种情况都能取到对应数据。
   ```
   const channel = supabase
     .channel('all-messages')
     .on('postgres_changes', {
       event: '*',
       schema: 'public',
       table: 'messages'
     }, (payload) => {
       console.log('变更类型:', payload.eventType)
       console.log('变更数据:', payload.new || payload.old)
     })
     .subscribe()
   ```
   
5. **带过滤条件的监听**
   聊天室按房间（room）划分，客户端通常只关心当前所在房间的消息，不希望接收其他房间的变动。这通过filter限定监听范围，让服务端只推送满足条件的行，减少无关回调。
   以下只监听messages表中room_id为"room-1001"的变更，即仅当该房间的消息发生变动时才通知。
   ```
   const channel = supabase
     .channel('room-1001-messages')
     .on('postgres_changes', {
       event: '*',
       schema: 'public',
       table: 'messages',
       filter: 'room_id=eq.room-1001'
     }, (payload) => {
       console.log('房间消息变更:', payload.new || payload.old)
     })
     .subscribe()
   ```
   filter格式为：\<列名\>=\<操作符\>.\<值\>，支持的操作符如[表3]所示。
    表3filter支持的操作符 
   | 操作符 | 含义    | 示例                            |
   |:---|:---|:---|
   | eq     | 等于     | room_id=eq.room-1001             |
   | neq  | 不等于  | room_id=neq.room-1001               |
   | gt    | 大于   | created_at=gt.2026-01-01          |
   | gte   | 大于等于 | created_at=gte.2026-01-01        |
   | lt   | 小于   | created_at=lt.2026-01-01        |
   | lte  | 小于等于  | created_at=lte.2026-01-01        |
   | in      | 包含    | room_id=in.(room-1001,room-1002) |
      
   以上内容聚焦单一场景分频道演示，实际项目中，一个频道可用多个.on()同时监听不同表与事件，无需为每类变更各开一个频道。
   
 
 #### 消息广播（Broadcast）
[监听数据变更（Postgres Changes）]是"数据库发生变化时通知你"，Broadcast则相反，是"其他客户端发出消息时通知你"。发送方将消息投递到频道，Realtime立即转发给该频道所有在线订阅者。需要注意：消息只发送给此刻在线的客户端，不持久化保存；数据不经过数据库，仅在客户端之间经Realtime中转。聊天室里的"对方正在输入..."提示正是典型用法：输入状态瞬息变化、无需落库，用Broadcast在客户端之间即时传递。下面以聊天室为例，介绍Broadcast的发送与接收。
1. **创建频道并监听**
   场景：用户在输入框打字时，需要使同房间其他人看到"对方正在输入..."提示，停止输入则提示消失。此类瞬时状态无需写入数据库、也不必留存，使用Broadcast在客户端之间直接互发。
   以下创建一个广播频道并监听typing事件：channel()创建频道并完成配置，on()登记要监听的事件名，subscribe()上线。配置中self: false表示发送方收不到自己发出的广播，避免自己打字时也提示自己；如果希望收到自己的消息，则设为true（例如调试时）。此处presence:false仅为聚焦Broadcast演示，实际聊天室的同一频道可同时启用broadcast与presence。
   ```
   const channel = supabase.channel('chat:room-1001', {
     config: {
       broadcast: { self: false },  // 不接收自己发送的消息
       presence: false
     }
   })
   channel.on('broadcast', { event: 'typing' }, (payload) => {
     console.log('收到输入状态:', payload)
   })
   channel.subscribe()
   ```
   
2. **发送广播消息**
   用户开始或停止输入时，向频道发送一条typing广播，通知同房间其他人。通过channel.send()发送：type固定为'broadcast'，event为自定义事件名（用于与监听端匹配、区分不同类型的消息），payload为携带的任意数据（此处带用户名与是否正在输入）。服务端收到后转发给该频道在线订阅者。
   ```
   await channel.send({
     type: 'broadcast',
     event: 'typing',
     payload: { name: '小张', typing: true }
   })
   ```
   
 
#### 在线状态（Presence）
[监听数据变更（Postgres Changes）]解决了消息持久化与实时同步，[消息广播（Broadcast）]解决了瞬时状态互发，但二者都无法回答"此刻频道里有哪些人在线"。Presence正是用于补充这一能力，跟踪频道中的在线用户。
底层采用CRDT：一种多端状态同步算法，保证各客户端最终看到一致的在线名单。聊天室的在线成员列表、"xx加入了聊天"提示即由它驱动。成员的展示名与头像取自[配置启用]中定义的profiles表，是否在线、加入、离开等状态仅在频道内同步，不入库。
完整聊天室中，同一频道可同时承载Broadcast与Presence，在config中同时配置二者即可。下面以聊天室为例，介绍Presence功能。
1. **上线并注册状态。**
   用户进入聊天页，需立即将自己加入其他人的"在线成员"列表。做法是订阅聊天频道，订阅成功后调用track()登记自身状态（展示名、头像）。
   - 创建频道时通过presence.key为当前用户指定唯一标识，用于区分同一用户的多个设备（手机和电脑同时在线仍算同一人）。
   
   - 需在subscribe()的回调中确认**status === 'SUBSCRIBED'**后再track()，否则频道尚未握手成功，状态无法登记。
   
   
   以下示例中name与avatar取自profiles表当前用户记录。
   ```
   const channel = supabase.channel('chat:room-1001', {
     config: { presence: { key: 'user-42' } }
   })
   channel.subscribe(async (status) => {
     if (status === 'SUBSCRIBED') {
       await channel.track({
         name: '小张',
         avatar: '力'
       })
     }
   })
   ```
   
2. **同步在线成员列表** **。**
   自己上线后，还需获取频道成员列表以渲染在线列表。这通过sync事件实现：各方状态合并完成时触发，此时调用presenceState()可获取频道中所有人的最新状态，适合一次性渲染完整在线名单。
   聊天室通常这样配合：sync时全量刷新列表，再用下文的join/leave做增量更新（谁进入、谁离开）。
   ```
   channel.on('presence', { event: 'sync' }, () => {
     const state = channel.presenceState()
     console.log('当前在线成员:', Object.values(state).flat().map(p => p.name))
   })
   ```
   
3. **监听成员加入与离开** **。**
   全量列表之外，还需实时感知"有人进入/有人离开"，以弹出"xx加入了聊天"的提示。这通过join和leave事件实现：join在有人上线时触发，leave在有人下线时触发，回调分别给出加入与离开的成员状态，据此向列表增删成员。
   "离开"触发方式：当对方关闭页面或调用[取消订阅与清理]中的取消订阅时，一旦断开，Realtime即判定其下线，向其他人推送leave。
   ```
   channel.on('presence', { event: 'join' }, ({ newPresences }) => {
     console.log('成员加入:', newPresences.map(p => p.name))
   })
   channel.on('presence', { event: 'leave' }, ({ leftPresences }) => {
     console.log('成员离开:', leftPresences.map(p => p.name))
   })
   ```
   
4. **更新自身展示状态** **。**
   成员的展示状态可能变化，例如主动切换"忙碌"或"离开"标识，供其他人看到。做法是状态变化时再调用一次track()覆盖自身状态，其他成员通过sync/join感知变化并更新列表。
   注意track()是整体覆盖：每次都需传入完整状态（name、avatar、status缺一不可），而非仅提交发生变化的字段，漏传的字段会被清空。
   ```
   // 切换为忙碌
   await channel.track({ name: '小张', avatar: '力', status: '忙碌' })
   // 切换回在线
   await channel.track({ name: '小张', avatar: '力', status: '在线' })
   ```
   status仅为频道内展示状态（如"忙碌"、"在线"），不写入profiles表，profiles只存固定的展示名与头像。
   "对方正在输入..."这种瞬息状态不适合放Presence（频繁track开销大、且需即时消失），在[消息广播（Broadcast）]中已用Broadcast承载。
   
**完整聊天室示例：**一个频道、一次subscribe()即可同时承载消息同步、输入提示与在线名单。
```
const channel = supabase.channel('chat:room-1001', {
  config: {
    broadcast: { self: false },
    presence: { key: 'user-42' }
  }
})
  // 消息实时同步（Postgres Changes）
  .on('postgres_changes',
    { event: '*', schema: 'public', table: 'messages',
      filter: 'room_id=eq.room-1001' },
    (payload) => updateMessageList(payload)
  )
  // 输入提示（Broadcast）
  .on('broadcast', { event: 'typing' },
    (payload) => showTypingHint(payload)
  )
  // 在线成员（Presence）
  .on('presence', { event: 'sync' },
    () => renderOnlineList()
  )
  .subscribe()
```
上面示例中的updateMessageList、showTypingHint、renderOnlineList为业务侧示意函数，仅标注调用位置，需自行实现。
 #### 取消订阅与清理
页面或组件卸载时要取消订阅、释放连接：removeAllChannels()表示断开所有频道，channel.unsubscribe()表示只断开指定频道。对于Presence，断开还会触发频道里其他人的leave事件。
```
// 取消所有频道
supabase.removeAllChannels()
// 取消指定频道
channel.unsubscribe()
```
建议在组件卸载或页面关闭时调用清理方法，避免连接泄漏。
#### 参考文档
以下为Supabase开源相关资料，仅供参考。
- [Supabase Realtime入门](https://supabase.com/docs/guides/realtime/getting_started?queryGroups=language&language=ts)
- [Supabase JavaScript SDK（channel）](https://supabase.com/docs/reference/javascript/subscribe)
- [Supabase JavaScript SDK（on postgres_changes）](https://supabase.com/docs/reference/javascript/on-postgres-changes)
- [Supabase Broadcast指南](https://supabase.com/docs/guides/realtime/broadcast)
- [Supabase Presence指南](https://supabase.com/docs/guides/realtime/presence)
- [Supabase Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security)
 
