首页
/ Supabase Realtime:基于 WebSocket 的实时数据同步机制与三大能力实战(Database Changes、Presence、Broadcast)

Supabase Realtime:基于 WebSocket 的实时数据同步机制与三大能力实战(Database Changes、Presence、Broadcast)

2026-09-06 16:36:34作者:裴麒琰

Supabase Realtime 通过 WebSocket 将 Postgres 数据库状态与所有已连接客户端做实时同步,是 Supabase 构建 Web、移动端与 AI 应用时的实时通信底座。本篇基于仓库中的 Realtime 概览文档 展开,结合仓库内 Realtime 编码规范授权聊天示例 SupaSecureSlackPresence 示例 的源码,讲解 Database Changes、Presence、Broadcast 三项能力的调用方式、RLS 授权模型与生产级最佳实践。读完后你可以独立完成:带私有通道的实时聊天系统、在线用户状态跟踪,以及基于数据库触发器的事件广播。

授权通道聊天 Demo 中两名用户均成功连接并发送消息的运行截图

一、能力总览:Realtime 的三大功能

Realtime 概览文档 将 Supabase Realtime 的核心能力归纳为三类,三者共享同一条 WebSocket 连接与 Channel(频道)抽象:

1. Database Changes(数据库变更监听)

实时监听 Postgres 的 INSERT、UPDATE、DELETE 事件。你可以订阅指定表、按列过滤条件筛选,只接收关心的变更。底层由 Postgres 逻辑复制(Logical Replication,即 CDC,变更数据捕获)驱动。

2. Presence(在线状态同步)

在所有连接客户端之间存储并同步"在线用户状态"。可以跟踪谁在线、正在浏览哪个页面、光标位置在哪里。客户端断开连接时,对应的状态会被自动清理——这一点在 Presence 示例 中得到验证:客户端只需在 SUBSCRIBED 状态后调用 channel.track(...) 上报自己的状态,服务端负责在断连时回收。

3. Broadcast(广播消息)

向订阅了同一 Channel 的所有客户端发送任意消息。适用于输入指示器(typing indicator)、实时光标、游戏状态、通知推送等任何不需要持久化的实时通信场景。

二、技术细节:传输层与授权模型

概览文档给出的技术要点如下,这是理解后续所有代码示例的前提:

维度 取值
传输协议 WebSockets
应用层协议 Phoenix Channels
数据库集成 Postgres 逻辑复制(CDC)
客户端库 JavaScript(supabase-js)、Dart(Flutter)、Swift、Kotlin
授权机制 数据库变更订阅应用 Row Level Security(RLS)策略
扩展方式 可水平扩展至多个节点

从授权模型看,Realtime 的关键设计是:私有通道的权限校验直接落在 Postgres RLS 上。具体做法是对 realtime.messages 表(服务端自动生成的 realtime schema 内的表)编写 SELECT/INSERT 策略,策略中通过 realtime.topic() 函数取当前消息的 topic、用 auth.uid() 取当前用户 ID 做业务判断。SupaSecureSlack 示例 README 中的策略即典型写法:

CREATE POLICY "authenticated can read broadcast and presence state"
ON "realtime"."messages"
AS PERMISSIVE FOR SELECT
TO authenticated
USING (
  EXISTS (
    SELECT 1
    FROM public.rooms_users
    WHERE user_id = (select auth.uid())
    AND room_topic = realtime.topic()
    AND realtime.messages.extension in ('broadcast', 'presence')
  )
);

CREATE POLICY "authenticated can send broadcast and track presence"
ON "realtime"."messages"
AS PERMISSIVE FOR INSERT
TO authenticated
WITH CHECK (
  EXISTS (
    SELECT 1
    FROM public.rooms_users
    WHERE user_id = (select auth.uid())
    AND room_topic = realtime.topic()
    AND realtime.messages.extension in ('broadcast', 'presence')
  )
);

其中 extension in ('broadcast', 'presence') 这个条件很关键:同一张 realtime.messages 表同时承载 Broadcast 与 Presence 两类消息,策略需要按消息类型分别放行。Flutter Figma 克隆示例的迁移脚本 则展示了更通用的项目成员模型,直接把 realtime.topic() 当作 project_id 解析并复用 is_project_member() 函数:

create policy "Project members can receive presence and broadcast messages."
on realtime.messages for select using (is_project_member(realtime.topic()::uuid));
create policy "Project members can send presence and broadcast messages."
on realtime.messages for insert with check (is_project_member(realtime.topic()::uuid));

从这两处实现可以推断:只要 topic 命名与业务实体 ID 对齐(如 room:123),RLS 策略就能用一套通用的"成员校验函数"覆盖所有通道,无需为每个房间单独写策略。

三、Broadcast 实战:私有通道与消息收发

Broadcast 的核心客户端 API 是 supabase.channel() 的建连配置与 channel.send() 的发送调用。SupaSecureSlack 的受保护页面 展示了完整的"建频道—监听—订阅—追踪"链路:

let newChannel = supabase.channel(selectedRoom, {
  config: {
    broadcast: { self: true },
    private: true, // 告知服务端使用私有通道,触发 RLS 授权校验
  },
})

newChannel
  .on('broadcast', { event: 'message' }, ({ payload: payload }) =>
    addMessage(payload.user_id == user?.id, false, payload.message)
  )
  .subscribe((status, err) => {
    if (status == 'SUBSCRIBED') {
      setChannel(newChannel)
      newChannel.track({ email: user?.email }) // 订阅成功后再上报 presence 状态
    }
    if (status == 'CHANNEL_ERROR') {
      setError(err?.message || null) // RLS 拒绝授权时会落到这个状态
    }
  })

发送消息则是一条结构化的 channel.send 调用:

channel?.send({
  type: 'broadcast',
  event: 'message',
  payload: { message, user_id: user?.id },
})

Realtime 编码规范 对通道配置项给出了完整注释,建议建连时按如下结构组织:

const channel = supabase.channel('room:123:messages', {
  config: {
    broadcast: { self: true, ack: true },
    presence: { key: 'user-session-id', enabled: true },
    private: true, // 使用 RLS 授权时必须开启
  },
})

各配置项含义:

  • broadcast.self: true:让发送者自己也能收到自己发的广播(默认收不到,需自行回显);
  • broadcast.ack: true:服务端收到消息后回一个确认(acknowledgment);
  • presence.key:用于标识 presence 状态的自定义键(例如用户会话 ID);
  • presence.enabled:开启该通道的 presence 跟踪;只要客户端注册了 on('presence') 监听,客户端库会自动置位,无需手动设置;
  • private: true:要求认证并应用 RLS 策略。使用数据库触发器(realtime.broadcast_changes)或依赖 RLS 的通道必须设为私有。规范明确建议:生产环境优先私有通道,默认 private: false 的公共通道不建议在生产中使用。

数据库变更:postgres_changes 与"触发器 + broadcast"两种路线

Database Changes 能力原生由 Postgres 逻辑复制驱动,客户端历史上用 postgres_changes 事件订阅表变更。但仓库内的 Realtime 编码规范 给出了当前仓库推荐的做法,值得重点关注:

新应用不要使用 postgres_changes(单线程、扩展性受限);数据库变更通知应改用 broadcast + 数据库触发器(调用 realtime.broadcast_changesrealtime.send)。

规范的"函数选型决策表"把选型讲得很直白:

使用场景 推荐函数 不选 postgres_changes 的原因
自定义载荷、带业务逻辑 broadcast 更灵活、性能更好
数据库变更通知 触发器 + broadcast 更易扩展、载荷可定制
高频更新 broadcast + 精简载荷 吞吐量与控制力更好
用户在线/状态跟踪 presence(谨慎使用) 专门用于状态同步
客户端间通信 不带触发器的 broadcast(仅走 WebSocket) 更灵活、性能更好

对应的数据库侧写法,规范给了一个"通用捕获"触发器函数——广播到以"表名:行 ID"命名的 topic:

CREATE OR REPLACE FUNCTION notify_table_changes()
RETURNS TRIGGER AS $$
SECURITY DEFINER
LANGUAGE plpgsql
AS $$
BEGIN
  PERFORM realtime.broadcast_changes(
    TG_TABLE_NAME || ':' || COALESCE(NEW.id, OLD.id)::text,
    TG_OP,
    TG_OP,
    TG_TABLE_NAME,
    TG_TABLE_SCHEMA,
    NEW,
    OLD
  );
  RETURN COALESCE(NEW, OLD);
END;
$$;

需要更精细控制时,可以按表写专用触发器(topic 用 room: 前缀 + room_id),并支持条件广播——只在字段真正变化时推送:

-- 仅广播有意义的变更
IF TG_OP = 'UPDATE' AND OLD.status IS DISTINCT FROM NEW.status THEN
  PERFORM realtime.broadcast_changes(
    'room:' || NEW.room_id::text,
    TG_OP, TG_OP, TG_TABLE_NAME, TG_TABLE_SCHEMA, NEW, OLD
  );
END IF;

realtime.send 用于发送不绑定表结构的自定义消息,例如把 {id, status}status_changed 事件推给某个房间。需要注意两个安全细节:realtime.broadcast_changes 默认要求私有通道(规范说明这是为防止安全事故而设计),并且这些数据库函数不应在客户端代码里调用

postgres_changes 迁移到"触发器 + broadcast"时,规范给出了三步走:客户端把 .on('postgres_changes', { event: '*', schema, table }) 替换为对 INSERT/UPDATE/DELETE 三个 broadcast 事件的监听;数据库加触发器;再为 realtime.messages 配好 SELECT 授权策略。

四、Presence 实战:在线用户的追踪与自动清理

Presence 的完整生命周期在 nextjs-auth-presence 示例 中体现得很清楚,四步走:

// 1. 建通道时用 presence.key 声明"以谁为粒度追踪"
const channel = supabaseClient.channel('online-users', {
  config: { presence: { key: this_user?.email ?? 'Unknown' } },
})

// 2. 监听 presence.sync,拉取全量在线状态
channel.on('presence', { event: 'sync' }, () => {
  const presentState = channel.presenceState()
  setUserState({ ...presentState })
})

// 3. 监听增量事件 presence.join / presence.leave
channel.on('presence', { event: 'join' }, ({ newPresences }) => {
  console.log('New users have joined: ', newPresences)
})

// 4. 订阅成功后调用 track 上报自己的状态
channel.subscribe(async (status) => {
  if (status === 'SUBSCRIBED') {
    await channel.track({ user_name: this_user?.email ?? 'Unknown' })
  }
})

要点是:sync 事件给出通道内 presence 的完整快照,join/leave 给出增量;状态以 presence.key 为键聚合(同一 key 的多次 track 会覆盖更新)。客户端断开后状态由服务端自动清理,因此"在线列表"天然准确,无需额外心跳逻辑。规范同时提醒:presence 属于"专用能力",应节制使用(在线状态、计数器),高频业务数据走 broadcast。

五、私有通道授权:从 Demo 看完整落地

SupaSecureSlack 示例 是仓库内最完整的"Realtime 授权"参考实现,目标是用带授权的私有通道构建聊天系统:用户可建房间(room)、把他人拉进房间、发送不持久化的消息。整个授权链路由三张表 + 策略构成:

  1. 建表public.profiles(用户资料,由 auth 触发器自动写入)、public.rooms(房间,topic 唯一)、public.rooms_users(房间-用户关联),全部 ENABLE ROW LEVEL SECURITY
  2. 授权策略:除三张业务表的策略外,核心是上文第二节的 realtime.messages SELECT/INSERT 策略——用户只有在 rooms_users 中存在对应 (user_id, room_topic) 记录时,才能读写该 topic 的广播与 presence;
  3. 触发器insert_user() 函数 + on_new_auth_create_profile 触发器,保证新用户注册时 profiles 同步生成一行。

客户端侧只有一个"开关":建通道时声明 config: { private: true }。示例 README 特别注明了版本前提——需使用 @supabase/realtime-js v2.44.0 或更高版本才支持私有通道配置。运行效果即前文截图所示:双方都在房间中时可互发消息;一旦某用户不在 rooms_users 中,其订阅会以 CHANNEL_ERROR 状态失败,页面提示 "You do not have access to this room"(对应 page.tsx 中的错误处理分支)。

此外,规范建议在 Dashboard 的 Realtime Settings 中开启 private-only channels 强制所有通道走私有模式,进一步杜绝公共通道被误用。

六、Topic 命名、扩展性与重连机制

Topic 命名规范

Realtime 编码规范 给出了一套可直接套用的约定:

  • Topic 模式scope:entityscope:entity:id,例如 room:123:messagesgame:456:movesuser:789:notifications
  • Event 模式entity_action(snake_case),例如 message_createduser_joinedgame_ended;避免 updatechange 这类泛化命名;
  • 使用细粒度专属 topic 而非全局大 topic:消息只投递给真正订阅者,能显著降低网络流量、提升并发容量、让 RLS 策略更精准(对比 global:notificationsroom:${roomId}:messages)。

性能与扩展建议

规范给出的运维向建议包括:每个逻辑作用域用一个通道;高流量 topic 可分片(chat:shard:1chat:shard:2);关注 Realtime Settings 中的 Database connection pool size 配置;RLS 策略涉及的所有列都应建索引(规范示例中专门建了 idx_room_members_user_room 复合索引)。

重连与通道状态机

客户端内置指数退避自动重连与断线后自动重订阅,可通过 reconnectAfterMs 调整重试节奏,log_level: 'info' 可打开调试日志。channel.subscribe 回调会报告四种状态,这也是错误处理的骨架:

  • SUBSCRIBED:连接成功(含重连成功),开始收消息;
  • TIMED_OUT:连接尝试超时;
  • CLOSED:通道关闭;
  • CHANNEL_ERROR:发生错误(RLS 拒绝即落此状态),客户端会自动重试。

框架集成时,规范给出的 React 模式值得照搬:用 useRef 缓存 channel、订阅前检查 channel.state 防止重复订阅、必须在 cleanup 中调用 supabase.removeChannel()——"包含清理逻辑"被列进了代码生成检查清单。

七、常见使用场景

概览文档列出的典型场景与三种能力的对应关系如下:

  • 协作编辑与实时光标 → broadcast(光标/选区等高频临时状态)+ presence(协作者身份);
  • 聊天与消息应用 → 私有通道 + broadcast + RLS(SupaSecureSlack 即完整范例);
  • 实时仪表盘与分析面板 → 触发器 + broadcast,或数据库变更订阅;
  • 多人游戏 → broadcast(游戏状态、动作)+ 细粒度 topic(game:123:moves);
  • 通知与动态流realtime.send 或 broadcast(user:456:notifications);
  • 拍卖与竞价系统 → 触发器 + broadcast 保证竞价事件的可靠推送。

八、参考资料

登录后查看全文
热门项目推荐
相关项目推荐