基于 Supabase Realtime Presence 与 Auth 构建 Next.js 在线用户状态应用
本文基于开源仓库 supabase 中 examples/realtime/nextjs-auth-presence 官方示例,系统讲解如何将 Supabase Auth 与 Realtime Presence 结合,在 Next.js 应用中实时展示"当前谁正在查看页面"。读完本文你将掌握 channel 的创建与订阅、presence 的 track/join/sync 事件流,以及服务端渲染下基于 cookie 的会话鉴权实现,可直接复用到聊天室、在线协作文档、协作白板等多人实时场景。
示例项目定位与技术栈
该示例演示的是 Realtime Presence 的核心价值:同一时刻、同一个 channel 上,每个连接的客户端向其他客户端广播"我在这里"的状态。用户通过 Supabase Auth 登录后,页面即可看到所有"在线且正在浏览该页面"的用户列表。
项目技术构成(见 package.json):
- 前端框架:Next.js(^13.1.6)+ React 18;
- Supabase 客户端:
@supabase/ssr(latest)与@supabase/supabase-js(^2),其中 supabase-js v2 内置了对 Realtime Presence 的完整支持; - 后端:Supabase 托管的 Postgres 数据库(含 Realtime 能力),通过 supabase.com/dashboard 创建与管理。
从源码结构看(pages/ 下有 index.tsx、login.tsx、profile.tsx 与 _app.tsx),整个应用围绕三个页面展开:/(在线用户主页)、/login(登录/注册)、以及基于 SSR 鉴权的 Profile 页面,路由采用 Next.js Pages Router 约定。
理解 Realtime Presence 的三个核心事件
Presence 是 Realtime 提供的"用户在线状态"通道能力。示例中 pages/index.tsx 展示了三类 API 的组合用法,它们是理解整段逻辑的钥匙:
channel.on('presence', { event: 'sync' }, callback):当频道内 presence 状态整体同步(任何客户端加入或离开导致状态集变化)时触发,回调中通过channel.presenceState()获取当前完整在线状态集合,用于刷新 UI 列表;channel.on('presence', { event: 'join' }, callback):有新客户端加入并track自己的状态时触发,回调参数中的newPresences携带新加入者的状态载荷;channel.subscribe(callback):执行真实订阅。回调参数status为'SUBSCRIBED'时表示连接成功,此时才能调用channel.track(...)把自己的状态写入频道。
在示例中还有一个 config.presence.key 的设置(订阅时传入),用于把当前用户的 email 作为该用户在频道内的状态 key——这决定了 presenceState() 返回对象中键的组织方式,是后续渲染"用户列表"的依据。
从零开始的完整搭建步骤
第一步:创建 Supabase 项目并启用用户管理
- 注册并登录 supabase.com/dashboard,创建一个新项目,等待数据库启动完成;
- 运行 "User Management Starter" Quickstart。该操作会为用户管理创建 user 相关的数据表与 profile 表——Auth 登录后返回的
User对象(含 email)即来自这一套用户体系,本示例全程依赖 email 作为展示身份; - 在 Project Settings(齿轮图标)→ API 页签中,记录 API URL 与 publishable key(anon key),后续配置环境变量需要用到。
说明:示例中的
publishablekey 即客户端 API key,它在用户登录前提供"匿名访问"能力;用户登录后,请求会自动切换到用户自己的登录令牌(session),实现以当前用户身份访问数据库。
第二步:拉取示例并配置环境变量
将本仓库(或直接使用其中的 nextjs-auth-presence 目录)拉到本地后,在项目根目录创建 .env.local 文件。仓库已提供模板 sample.env.local,内容结构如下:
NEXT_PUBLIC_SUPABASE_URL="replace-this-with-your-supabase-instance"
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY="replace-this-with-your-supabasedb-anon-key"
将两个占位符分别替换为第一步取得的 API URL 与 publishable key。由于两个变量都以 NEXT_PUBLIC_ 开头,它们会在构建期被内联进浏览器端 bundle,供客户端代码使用——这正是 Presence 这类纯客户端实时功能所必需的前提。
第三步:安装依赖并启动开发服务器
npm install
npm run dev
用浏览器打开 http://localhost:3000 即可看到效果。项目自身提供的 dev/build/start/lint/format 脚本定义在 package.json 中,除 npm run dev 外,也支持 npm run build && npm start 以生产模式运行。
源码拆解:Auth 上下文与 Presence 主逻辑
全局 Supabase 客户端与用户状态(lib/supabase-context.tsx)
lib/supabase-context.tsx 是整个应用的"地基",它做了三件事:
- 模块级调用
createBrowserClient创建单例浏览器端客户端,读取两个NEXT_PUBLIC_环境变量作为参数; - 通过 React Context 向全树暴露
supabase客户端与user状态; - 在
useEffect中调用supabase.auth.getUser()初始化用户,并订阅onAuthStateChange,使得登录态变化时(session?.user)能同步刷新user。
对外暴露的 useSupabaseClient() 与 useUser() 两个 Hook,被登录页和主页直接消费——组件从 Context 拿客户端与当前用户,不需要各自创建实例。
同时,_app.tsx 中的 MyAppInner 也在监听 onAuthStateChange,并在 SIGNED_IN 时跳转 /、SIGNED_OUT 时跳转 /login,实现全局性的登录态驱动路由。注意 SIGNED_IN 事件同时被 supabase-context.tsx 与 _app.tsx 两处使用:前者负责更新 React 状态,后者负责页面导航,职责互补。
在线用户列表(pages/index.tsx 的 Presence 逻辑)
pages/index.tsx 的组件侧核心代码如下:
const channel = supabaseClient.channel('online-users', {
config: {
presence: {
key: this_user?.email ? this_user?.email : 'Unknown',
},
},
})
channel.on('presence', { event: 'sync' }, () => {
const presentState = channel.presenceState()
setUserState({ ...presentState })
})
channel.on('presence', { event: 'join' }, ({ newPresences }) => {
console.log('New users have joined: ', newPresences)
})
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
const status = await channel.track({
user_name: this_user?.email ? this_user?.email : 'Unknown',
})
console.log('status: ', status)
}
})
关键流程:
- 创建名为
online-users的频道,并把用户 email(未登录时为'Unknown')设为 presence key; sync事件到来后用channel.presenceState()全量快照刷新 React state;- 订阅成功后
track当前用户(载荷为{ user_name: email }),告诉频道"我在线"; - 视图层遍历
userState的 key(即各在线用户的 email key),渲染出"Hi xxx"列表,同时提供 Sign out 按钮(见 pages/index.tsx)。
需要指出:示例中 useEffect 的依赖数组为空 [],意味着频道创建与订阅只在页面挂载时执行一次;useState 中 userState 以 {} 初始化并随后被 sync 快照覆盖。若在真实项目里复用此模式,建议关注组件卸载时的退订清理,并考虑把用户 email 变更纳入依赖,避免多用户切换时状态错乱。
服务端鉴权:getServerSideProps 中的 createServerClient
pages/index.tsx 同文件里还演示了 Pages Router 下的 SSR 会话校验:
- 使用
createServerClient(url, key, { cookies })创建服务端客户端,其中cookies.getAll()读取请求 Cookie,cookies.setAll()把需要写入的 session cookie 序列化后写回响应头(依赖cookie包的serialize); - 随后
supabase.auth.getUser()校验会话,若没有 user 则redirect到/login; - 校验通过则将
user作为 props 传给页面组件。
同样的 cookie 注入模式也完整复刻在 pages/profile.tsx 中,说明这是该示例在服务端保护路由时的统一写法——它是 @supabase/ssr 在 Next.js Pages Router 中的标准集成姿势。
登录/注册页(pages/login.tsx)
pages/login.tsx 实现了一个可在 sign-in / sign-up 间切换的表单:
signInWithPassword({ email, password })处理登录;signUp({ email, password, options: { emailRedirectTo } })处理注册,注册后若无 session(即需要邮件确认时),页面提示用户查收确认链接;- 已登录用户会直接渲染出完整 user 对象的 JSON 视图并提供登出按钮(pages/login.tsx)。
注意密码框的 minLength={6} 与浏览器端 autoComplete 属性,都是为配合 Supabase Auth 对密码的基础约束而设。
如何验证 Presence 效果
按 README 提供的测试路径逐项操作:
- 访问
http://localhost:3000,会被自动重定向到登录/注册页; - 未注册则先 Sign up,再使用邮箱 + 密码 Sign in;
- 登录后跳回主页,页面会列出你的 email(形如
Hi your@email.com); - 另开一个浏览器窗口,用另一个账号登录;
- 切回第一个窗口,观察在线用户列表已自动更新,新登录的账号出现在列表中。
验证要点:列表的实时更新依赖 sync 事件触发 setUserState 的 React 重渲染,因此无需手动刷新页面即可看到第二个用户出现;当第二个窗口登出或关闭时,第一个窗口的列表同样会自动移除该用户。打开浏览器 DevTools Console,可以看到 New users have joined: 与 status: 的日志输出,它们分别来自 join 事件回调和 track 的返回结果。
部署建议
由于这是一个 Next.js 应用,最简单的部署方式是推到 Vercel 平台(在 Vercel 项目面板中重新填入两个 NEXT_PUBLIC_ 环境变量后执行部署)。仓库内也保留了 Vercel 相关的默认静态资源(public/vercel.svg),说明其默认部署目标即 Vercel;若部署到自有服务器,则使用 npm run build && npm start 即可。无论何种方式,请确保生产环境变量中的 URL 指向可被公网访问的 Supabase 项目实例。
后续演进方向
README 的 "Conclusion/Next Steps" 部分给出了从示例走向完整产品的建议,与仓库中已有的页面骨架可以一一对应:
- 实现 Profile 页面:
pages/profile.tsx已具备 SSR 鉴权与用户信息展示,可扩展为可编辑资料的完整资料页; - 支持用户头像上传:可结合 Supabase Storage 上传头像并回写 profile 表;
- 读取社交账号头像:启用 Auth 的 OAuth 登录(如 GitHub、Google)后,可从第三方返回的
user_metadata中直接取到头像 URL。
此外,从工程角度,可将本示例与仓库同目录下的姊妹示例对照研读——examples/realtime/nextjs-authorization-demo 侧重 Realtime 频道级授权(Authorization),而本示例侧重 Presence 状态同步,两者结合可覆盖"谁能监听"与"谁在场"两个维度;仓库中的 Realtime 其余示例(如 Flutter 版多人游戏)也展示了 Presence 在更大并发场景下的应用形态。
小结
通过这一个约 300 行核心代码的示例,可以完整走通 Supabase 的"Auth 认证 → SSR 会话保护 → Realtime channel 订阅 → Presence track/join/sync 状态同步"整条链路。本文覆盖了官方 README 的全部操作步骤,并补充了 lib/supabase-context.tsx、pages/index.tsx、_app.tsx 与 pages/login.tsx 的源码级剖析。理解这四点,你就能在此基础上快速搭建属于自己的多人协作与在线状态产品。
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 StartedRust0623
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