资讯动态

使用 Supabase 与 Next.js 构建实时 Slack 克隆:从数据库 Schema、RLS 权限到 Realtime 全栈实践

发布时间:2026/9/7 2:48:38 来源:尧图企业网站定制
使用 Supabase 与 Next.js 构建实时 Slack 克隆从数据库 Schema、RLS 权限到 Realtime 全栈实践【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase本指南以仓库中的 nextjs-slack-clone 示例 为讲解主体剖析一个生产级实时聊天应用如何组合 Postgres 数据模型、行级安全Row Level Security、基于自定义 JWT Claim 的角色访问控制RBAC与 Supabase Realtime 订阅帮助你理解并复刻从建库、授权、部署到本地联调的完整闭环。示例概览技术栈与功能这是仓库examples/slack-clone/nextjs-slack-clone目录下的全栈示例目标是以一个可运行的 Slack 克隆演示 Supabase 的三大核心能力托管 Postgres、用户认证与实时数据同步。示例的技术分层如下前端Next.jsReact 框架负责页面渲染与路由客户端 SDKSupabase.jssupabase/supabase-js承担用户管理与实时数据监听后端由 supabase.com/dashboard 提供的托管 Postgres 数据库及其 RESTful API供 Supabase.js 直接调用。从 package.json 可以看到完整的依赖清单核心运行时依赖只有supabase/supabase-js^2、jwt-decode^4、next与react/react-dom^18.2前端样式使用 Tailwind CSS 3 与 PostCSS/Autoprefixer 处理整体依赖轻量、易于理解。示例实现的功能点包括邮箱注册登录、频道列表channels与消息流messages、消息与频道的实时增删同步、在线状态字段预留以及 admin / moderator 两种管理角色的差异化删除权限。其目录结构如下nextjs-slack-clone/ ├── components/ # Layout / Message / MessageInput / TrashIcon ├── lib/ │ ├── Store.js # supabase 客户端 数据读取、写入与实时订阅 Hook │ └── UserContext.js # 用户上下文由 _app.js 提供 ├── pages/ │ ├── _app.js # 会话恢复、JWT 解码与角色注入、登录重定向 │ ├── index.js │ └── channels/[id].js # 频道消息页 ├── public/slack-clone-demo.gif ├── supabase/ │ ├── config.toml # 本地开发配置API / DB / Realtime / Auth / Hook │ ├── migrations/ # init.sql 与 auth-hook.sql │ └── seed.sql # 角色权限与演示数据种子 ├── full-schema.sql # 与 Dashboard 端 Quickstart 一致的单文件完整 Schema └── .env(.production).example下图为示例的交互效果演示数据模型六张表与三类枚举无论通过 Supabase Dashboard 运行 Slack Clone Quickstart还是使用本地迁移文件建库最终都会得到相同的数据结构。单文件版见 full-schema.sql分步迁移版见 20240214102356_init.sql。自定义枚举类型create type public.app_permission as enum (channels.delete, messages.delete); create type public.app_role as enum (admin, moderator); create type public.user_status as enum (ONLINE, OFFLINE);业务表表关键字段语义usersid uuid引用auth.users主键、username、status默认OFFLINE用户资料id直接指向 Supabase Auth 的内部用户channelsslug text unique、created_by引用users频道Slack 中的 Channelmessagesmessage、user_id、channel_idon delete cascade消息频道删除时级联清理user_rolesuser_idrole联合唯一用户与角色的多对多关联role_permissionsrolepermission联合唯一角色与权限点的多对多映射值得注意的是channels与messages的inserted_at字段使用timezone(utc::text, now())生成统一 UTC 时间戳id则采用generated by default as identity避免显式序列依赖。Row Level Security以 Postgres 为安全边界启动 Supabase 项目时平台会自动预置authschema 与一批辅助函数。用户登录后会获得一个包含authenticated角色与自身 UUID 的 JWT这正是 RLS 策略进行精细化授权的依据。表级开启与策略Schema 对全部业务表执行了enable row level security并为每种操作定义了最小化授权策略。以channels为例create policy Allow logged-in read access on public.channels for select using ( auth.role() authenticated ); create policy Allow individual insert access on public.channels for insert with check ( auth.uid() created_by ); create policy Allow individual delete access on public.channels for delete using ( auth.uid() created_by ); create policy Allow authorized delete access on public.channels for delete using ( authorize(channels.delete) );设计要点如下读任何已登录用户auth.role() authenticated都能读取频道、消息与用户资料这与 Slack 默认开放的工作区属性一致写插入消息/频道时要求auth.uid()与记录所有者字段一致杜绝伪造他人身份的数据写入删采用“双轨制”——普通用户只能删除自己创建的内容同时叠加一条authorize(...)授权策略使拥有对应权限点的角色可以删除任意内容user_roles表仅允许用户读取自己的记录而完整的角色关系由后端的 auth hook 或security definer函数持有避免客户端越权窥探权限映射。authorize授权函数删除策略中调用的public.authorize是一个security definer函数它从当前访问令牌 JWT 中取出自定义 Claimuser_role再与role_permissions表做匹配若count 0则放行否则拒绝。关键实现见 full-schema.sqlselect count(*) from public.role_permissions where role_permissions.permission authorize.requested_permission and role_permissions.role (auth.jwt() - user_role)::public.app_role into bind_permissions; return bind_permissions 0;为什么 RLS 是本示例的安全核心客户端 SDK 默认使用anon公钥直连 PostgREST。anon公钥允许“匿名访问”数据库直到用户登录——登录后请求会自动携带用户自己的登录令牌。真正阻止越权的不是密钥本身而是每一张表上的 RLS 策略。这就是本示例强调的“使用 Postgres 行级安全实现高级别授权”的含义即使anonkey 暴露在浏览器中未通过 RLS 策略的操作仍会被数据库拒绝。安全提醒service_role密钥拥有完整数据访问权限会绕过所有安全策略必须严格保密只能在服务端环境使用绝不能出现在浏览器或客户端代码中。RBAC用 plus addressing 自定义 JWT Claim 管理角色角色规则的注册规则示例约定通过邮箱的 plus 别名subaddressing自动分配角色// admin 用户 emailsupaadminexample.com // moderator 用户 emailsupamodexample.com该逻辑由触发器handle_new_user实现。每当auth.users有新用户插入on_auth_user_created触发器见 full-schema.sql就会自动执行以下工作insert into public.users (id, username) values (new.id, new.email); if position(supaadmin in new.email) 0 then insert into public.user_roles (user_id, role) values (new.id, admin); elsif position(supamod in new.email) 0 then insert into public.user_roles (user_id, role) values (new.id, moderator); end if;即为用户创建资料行并根据邮箱中是否包含supaadmin/supamod关键字写入对应角色。权限矩阵权限点与角色的映射由 seed.sql 预置insert into public.role_permissions (role, permission) values (admin, channels.delete), (admin, messages.delete), (moderator, messages.delete);由此得到完整的行为矩阵操作普通用户moderatoradmin删除自己的消息允许RLS允许允许删除任意消息禁止允许messages.delete允许删除任意频道禁止禁止允许channels.delete注意示例并不建议删除public频道——前端在路由层面也会把当前频道被删除的用户引导回public见下文源码分析。自定义 Access Token Hook把角色写进 JWTRLS 策略与authorize函数都依赖 JWT 中的user_roleClaim那么该 Claim 从何而来答案是 Auth Hook 自定义 Access Token HookAuth 服务在签发访问令牌前调用一个 Postgres 函数把数据库里的角色注入令牌。该函数定义在 20240214114147_auth-hook.sqlcreate or replace function public.custom_access_token_hook(event jsonb) returns jsonb language plpgsql stable as $$ declare claims jsonb; user_role public.app_role; begin select role into user_role from public.user_roles where user_id (event-user_id)::uuid; claims : event-claims; if user_role is not null then claims : jsonb_set(claims, {user_role}, to_jsonb(user_role)); else claims : jsonb_set(claims, {user_role}, null); end if; event : jsonb_set(event, {claims}, claims); return event; end; $$;配套的权限收敛同样关键仅授予supabase_auth_admin执行该函数与访问user_roles表的权限从authenticated、anon角色撤销执行与访问权限新增一条针对supabase_auth_admin的全表可读策略保证 hook 运行时能查询到角色数据。本地开发时Auth Hook 的启用声明在 supabase/config.toml[auth.hook.custom_access_token] enabled true uri pg-functions://postgres/public/custom_access_token_hook前端如何消费角色客户端不会直接接触user_roles表而是解码访问令牌获取角色。在 pages/_app.js 中每次会话建立时都会用jwt-decode解码access_token并把jwt.user_role挂到当前用户对象上const jwt jwtDecode(session.access_token) currentUser.appRole jwt.user_roleMessage/MessageInput等组件再依据user.appRole决定是否渲染删除按钮moderator 可删消息admin 额外可删频道界面权限与数据库权限保持一致。删除按钮的 UI 控制与底层deleteChannel/deleteMessage调用位于 components 与 lib/Store.js。Realtime让数据变化实时流向 UI逻辑层把表加入实时发布实时推送需要先把表加入 Postgres 的逻辑复制发布publication。Schema 的做法是先重置supabase_realtime发布再逐表加入begin; drop publication if exists supabase_realtime; create publication supabase_realtime; commit; alter publication supabase_realtime add table public.channels; alter publication supabase_realtime add table public.messages; alter publication supabase_realtime add table public.users;为了让订阅方能收到“完整旧值”例如删除事件的 payload 里能拿到被删记录的完整字段三张表均被设置为replica identity fullalter table public.users replica identity full; alter table public.channels replica identity full; alter table public.messages replica identity full;应用层postgres_changes 订阅前端数据层集中在 lib/Store.js。其核心是useStoreHook它在挂载时并行建立三条实时通道const messageListener supabase .channel(public:messages) .on(postgres_changes, { event: INSERT, schema: public, table: messages }, (payload) handleNewMessage(payload.new) ) .on(postgres_changes, { event: DELETE, schema: public, table: messages }, (payload) handleDeletedMessage(payload.old) ) .subscribe()订阅矩阵概括如下通道事件处理逻辑public:messagesINSERT追加消息若作者不在本地 Map 中先按user_id拉取作者再展示public:messagesDELETE从本地消息数组中过滤掉对应idpublic:channelsINSERT / DELETE向侧栏追加频道 / 移除频道public:users*任意事件更新本地用户名映射这些 Hook 收到变化后只修改本地 React 状态最终通过返回值把作者信息映射到消息上messages: messages.map((x) ({ ...x, author: users.get(x.user_id) })), channels: channels.sort((a, b) a.slug.localeCompare(b.slug)),这一模式单一客户端、多通道订阅、状态与数据库事件联动是理解 Supabase Realtime 的最短路径应用不需要自建 WebSocket 服务只需写数据库postgres_changes会把变更推送给所有订阅者。关键数据读写与路由编排数据访问函数lib/Store.js 以模块级函数暴露所有增删查操作fetchChannelssupabase.from(channels).select(*)拉取全部频道fetchMessages(channelId)连表查询消息作者并按时间升序排列supabase .from(messages) .select(*, author:user_id(*)) .eq(channel_id, channelId) .order(inserted_at, { ascending: true })这里的author:user_id(*)是 PostgREST 的内嵌资源语法——把外键user_id关联的 users 行重命名为author一次请求同时取回消息与其作者addChannel(slug, user_id)/addMessage(...)执行带.select()的插入便于拿到服务端返回的完整记录deleteChannel/deleteMessage.delete().match({ id })按主键删除删除是否被放行由 RLS authorize决定。会话编排与会话守卫pages/_app.js 是全局会话中枢应用启动时调用supabase.auth.getSession()恢复会话并注册supabase.auth.onAuthStateChange监听登录/登出事件一旦检测到登录用户立即把路由推进到频道页if (currentUser) { router.push(/channels/[id], /channels/1) }登出则调用supabase.auth.signOut()后回到首页。用户与登录状态通过UserContext见 lib/UserContext.js向下分发。pages/channels/[id].js 承载聊天主界面它把路由参数channelId传给useStore当消息列表变化时自动滚动到底部并在当前频道被删除时把用户安全重定向回public/channels/1useEffect(() { if (!channels.some((channel) channel.id Number(channelId))) { router.push(/channels/1) } }, [channels, channelId])这一“兜底频道”设计保证了演示环境中的public频道始终可达与该频道不建议删除的建议相呼应。四种落地方式从 Dashboard 到本地 CLI方式一Supabase Dashboard Quickstart最快上手到 supabase.com/dashboard 注册并创建一个新项目等待数据库启动数据库就绪后在 SQL Editor 中运行Slack Clone Quickstart即 full-schema.sql 的内容进入项目设置齿轮图标的API选项卡复制API URL与anon公钥供下一步配置环境变量使用。anonkey 是客户端 API 密钥它允许对数据库进行“匿名访问”用户登录后请求自动切换为该用户的登录令牌行级安全随即对数据生效。方式二一键部署到 VercelVercel 部署流程会引导你创建 Supabase 账号与项目安装 Supabase integration 后全部相关环境变量会自动配置完毕部署完成后项目即可直接使用。部署前建议在 Vercel 项目设置中核对以下环境变量它们决定了前端 SDK 的连接目标NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY方式三本地开发连接本地 Supabase本地开发配置以supabase/目录与.env文件为主复制环境变量模板cp .env.example .env模板内容见 .env.example默认指向本地实例NEXT_PUBLIC_SUPABASE_URLhttp://127.0.0.1:54321/ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEYlocal-anon-key NEXT_SITE_URLhttp://localhost:3000 NEXT_REDIRECT_URLShttp://localhost:3000/启动本地 Supabase 栈会按 supabase/config.toml 启动 API、Postgres、Realtime、Auth 与 Inbucket 邮件测试服务器并应用supabase/migrations中的迁移与seed.sql种子数据安装依赖并启动前端npm install npm run dev # 打开 http://localhost:3000在本地栈中config.toml 有几个值得注意的配置API 端口54321、数据库端口54322、major_version 15、[auth.hook.custom_access_token]已启用并且[inbucket]开启——注册确认邮件不会真实投递而是显示在 Inbucket 的 Web 界面中方便完成邮箱验证流程。方式四连接远程 Supabase 项目生产若直接使用远程项目可按以下流程把本地配置与远端同步在 Supabase Dashboard 创建或选择一个项目复制生产环境模板并填写真实值cp .env.production.example .env.production模板见 .env.production.example其中NEXT_SITE_URL/NEXT_REDIRECT_URLS需要替换为你真实的 Vercel 应用地址Redirect 列表同时支持精确地址与通配子路径如https://app.vercel.app/**关联本地项目与远程项目SUPABASE_ENVproduction npx supabaselatest link --project-ref your-project-ref推送配置auth、realtime、api等项目级设置SUPABASE_ENVproduction npx supabaselatest config push推送数据库 Schema迁移文件 种子数据SUPABASE_ENVproduction npx supabaselatest db push方式五Vercel Preview Branching 的隔离环境Supabase 与 Vercel 预览分支集成可以为每个 Git 分支创建独立的 Supabase 项目从而在安全隔离的环境中先行验证数据库迁移或服务配置再合入生产。操作步骤确认 Vercel 项目已关联 Git 仓库在 Vercel 中为Preview环境配置两个变量NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY新建分支并修改代码例如调整 config.toml 中的max_frequency邮件频率限制推送到 Git 并开启 Pull Request触发 Vercel Supabase 集成部署成功后在 Preview 环境即可验证改动效果确认无误后再合并到主分支。前端鉴权链路速查把 RLS、RBAC、Auth Hook 与前端解码串起来即得到本示例完整的授权闭环用户注册(含 supaadmin / supamod) └─ auth.users 插入 └─ on_auth_user_created 触发器 → users 资料行 user_roles 角色 └─ 登录签发令牌时custom_access_token_hook 将 user_role 注入 JWT claims └─ 客户端 jwtDecode 解出 user.appRole控制删除按钮显隐 └─ 后端删除请求再次经过 RLS 策略 authorize() 复核即界面权限只是体验层真正的安全决策永远发生在数据库层——即使有人绕过 UI 直接调用 REST API缺少channels.delete/messages.delete权限点的角色也会被authorize()函数拦截。小结nextjs-slack-clone是学习 Supabase 生产模式的最小而完整的范本。它示范了四件可迁移到任意业务的关键技能用Postgres 原生类型 外键 级联设计聊天域数据模型用RLS 策略 security definer授权函数让客户端直连数据库依然安全用自定义 Access Token Hook把 RBAC 角色注入 JWT让权限在数据库、后端与前端三层统一用Realtime Publication postgres_changes免自建消息服务实现多端实时同步。若想继续深挖实现细节推荐按顺序阅读以下仓库文件迁移文件 init、Auth Hook 迁移、完整单文件 Schema、种子数据 与 前端实时数据层。【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价