首页
/ 基于 Supabase Realtime Presence 与 Auth 构建 Next.js 在线用户状态应用

基于 Supabase Realtime Presence 与 Auth 构建 Next.js 在线用户状态应用

2026-09-06 18:53:30作者:冯梦姬Eddie

本文基于开源仓库 supabaseexamples/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.tsxlogin.tsxprofile.tsx_app.tsx),整个应用围绕三个页面展开:/(在线用户主页)、/login(登录/注册)、以及基于 SSR 鉴权的 Profile 页面,路由采用 Next.js Pages Router 约定。

理解 Realtime Presence 的三个核心事件

Presence 是 Realtime 提供的"用户在线状态"通道能力。示例中 pages/index.tsx 展示了三类 API 的组合用法,它们是理解整段逻辑的钥匙:

  1. channel.on('presence', { event: 'sync' }, callback):当频道内 presence 状态整体同步(任何客户端加入或离开导致状态集变化)时触发,回调中通过 channel.presenceState() 获取当前完整在线状态集合,用于刷新 UI 列表;
  2. channel.on('presence', { event: 'join' }, callback):有新客户端加入并 track 自己的状态时触发,回调参数中的 newPresences 携带新加入者的状态载荷;
  3. channel.subscribe(callback):执行真实订阅。回调参数 status'SUBSCRIBED' 时表示连接成功,此时才能调用 channel.track(...) 把自己的状态写入频道。

在示例中还有一个 config.presence.key 的设置(订阅时传入),用于把当前用户的 email 作为该用户在频道内的状态 key——这决定了 presenceState() 返回对象中键的组织方式,是后续渲染"用户列表"的依据。

从零开始的完整搭建步骤

第一步:创建 Supabase 项目并启用用户管理

  1. 注册并登录 supabase.com/dashboard,创建一个新项目,等待数据库启动完成;
  2. 运行 "User Management Starter" Quickstart。该操作会为用户管理创建 user 相关的数据表与 profile 表——Auth 登录后返回的 User 对象(含 email)即来自这一套用户体系,本示例全程依赖 email 作为展示身份;
  3. 在 Project Settings(齿轮图标)→ API 页签中,记录 API URLpublishable key(anon key),后续配置环境变量需要用到。

说明:示例中的 publishable key 即客户端 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 是整个应用的"地基",它做了三件事:

  1. 模块级调用 createBrowserClient 创建单例浏览器端客户端,读取两个 NEXT_PUBLIC_ 环境变量作为参数;
  2. 通过 React Context 向全树暴露 supabase 客户端与 user 状态;
  3. 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 提供的测试路径逐项操作:

  1. 访问 http://localhost:3000,会被自动重定向到登录/注册页;
  2. 未注册则先 Sign up,再使用邮箱 + 密码 Sign in;
  3. 登录后跳回主页,页面会列出你的 email(形如 Hi your@email.com);
  4. 另开一个浏览器窗口,用另一个账号登录;
  5. 切回第一个窗口,观察在线用户列表已自动更新,新登录的账号出现在列表中。

验证要点:列表的实时更新依赖 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.tsxpages/index.tsx_app.tsxpages/login.tsx 的源码级剖析。理解这四点,你就能在此基础上快速搭建属于自己的多人协作与在线状态产品。

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