Supabase Realtime:基于 WebSocket 的实时数据同步机制与三大能力实战(Database Changes、Presence、Broadcast)
Supabase Realtime 通过 WebSocket 将 Postgres 数据库状态与所有已连接客户端做实时同步,是 Supabase 构建 Web、移动端与 AI 应用时的实时通信底座。本篇基于仓库中的 Realtime 概览文档 展开,结合仓库内 Realtime 编码规范、授权聊天示例 SupaSecureSlack 与 Presence 示例 的源码,讲解 Database Changes、Presence、Broadcast 三项能力的调用方式、RLS 授权模型与生产级最佳实践。读完后你可以独立完成:带私有通道的实时聊天系统、在线用户状态跟踪,以及基于数据库触发器的事件广播。
一、能力总览: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_changes或realtime.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)、把他人拉进房间、发送不持久化的消息。整个授权链路由三张表 + 策略构成:
- 建表:
public.profiles(用户资料,由 auth 触发器自动写入)、public.rooms(房间,topic唯一)、public.rooms_users(房间-用户关联),全部ENABLE ROW LEVEL SECURITY; - 授权策略:除三张业务表的策略外,核心是上文第二节的
realtime.messagesSELECT/INSERT 策略——用户只有在rooms_users中存在对应(user_id, room_topic)记录时,才能读写该 topic 的广播与 presence; - 触发器:
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:entity或scope:entity:id,例如room:123:messages、game:456:moves、user:789:notifications; - Event 模式:
entity_action(snake_case),例如message_created、user_joined、game_ended;避免update、change这类泛化命名; - 使用细粒度专属 topic 而非全局大 topic:消息只投递给真正订阅者,能显著降低网络流量、提升并发容量、让 RLS 策略更精准(对比
global:notifications与room:${roomId}:messages)。
性能与扩展建议
规范给出的运维向建议包括:每个逻辑作用域用一个通道;高流量 topic 可分片(chat:shard:1、chat: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 保证竞价事件的可靠推送。
八、参考资料
- 概览文档:apps/www/content/md/realtime.md
- 编码规范与函数选型:examples/prompts/use-realtime.md
- 授权聊天 Demo(私有通道 + RLS):examples/realtime/nextjs-authorization-demo/README.md、核心页面实现
- Presence 示例:examples/realtime/nextjs-auth-presence/pages/index.tsx
- 项目成员模型的 Realtime RLS 迁移:examples/realtime/flutter-figma-clone/supabase/migrations/20240808072352_auth.sql
- 概览文档还指向 Supabase 官方在线文档(Realtime Guides 与 JavaScript subscribe API Reference),可进一步查阅细节。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
