Vibe Coding 时代的第一课:用 Next.js + Supabase 从零搭出带登录的 AI 聊天室
【免费下载链接】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 能在几分钟内生成整页的 React 组件、能按提示词把需求翻译成可运行的代码时,"编程"这件事的门槛被前所未有地拉低了。但 Vibe Coding 有一个残酷的真相:AI 擅长的是"写代码",而不是"兜底"。数据库迁移、用户认证、权限模型、实时消息推送——这些后端工程里最容易被 AI 糊弄过去的环节,恰恰是一个应用能不能真正上线、能不能扛住用户的关键。
这正是 Supabase 在 2025-2026 年持续被推上风口的原因。这家把自己定义为 "The Postgres development platform" 的开源公司,先后完成数轮大额融资并收购了数据库公司 Turso,估值走向百亿美元量级,被大量媒体称作 "vibe coding 的默认后端";在社区里,关于"用一个周末开发百万并发应用""如何用 Supabase Auth 五分钟接入登录注册"的教程长期霸占掘金与 CSDN 的阅读榜。它的核心叙事很清晰:给 AI 生成的"前端灵感"配一个能直接落地的 Postgres 后端。
但热度之下也有冷水。安全机构 UpGuard 的研究曾指出有超过 1.6 万个 Supabase 数据库暴露了可读取的表——这不是 Supabase 的缺陷,而是"默认开放"的 Postgres 权限模型与开发者安全意识缺失共同作用的结果。换句话说:会用 Supabase 和会用对 Supabase,是两回事。
本文不写空泛的"快速上手",而是直接以官方仓库中两个真实示例为蓝本,拆解一条完整的进阶路径:先用 Supabase Auth 把登录注册接到 Next.js 里,再靠 Postgres 的实时订阅把聊天消息广播出去,最后用行级安全(RLS)给每一行数据上锁。三步走完,你就拥有一个带登录、实时聊天、权限受控的最小聊天室——这也是绝大多数 AI 应用(Agent 会话、Copilot 对话流、协作白板)的第一层技术底座。
Supabase Auth:五分钟接入登录注册
先说结论:在 Supabase 里,用户系统不是一个"模块",而是数据库自带的一等公民。每个 Supabase 项目初始化时都会自动创建authschema,里面躺着auth.users表和整套 JWT 签发、刷新令牌、邮件验证机制。你不需要自己建 users 表、不需要自己写密码哈希、不需要自己管理 session——这些在 examples/user-management/nextjs-user-management 官方示例里都是"零新增代码"的开箱能力。
在 Next.js App Router 下,接入的关键是区分两个客户端:服务端用 cookie 承载会话,浏览器端直接用 anon key 连接。示例中的 lib/supabase/server.ts 展示了标准做法——用@supabase/ssr的createServerClient把 Supabase 的 session cookie 与 Next.js 的 cookie store 打通:
import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' export async function createClient() { const cookieStore = await cookies() return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, { cookies: { getAll() { return cookieStore.getAll() }, setAll(cookiesToSet, _headers) { try { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options) ) } catch { // 服务端组件中调用 setAll 可忽略——proxy 会负责刷新会话 } }, }, } ) }对应地,浏览器端只需要 lib/supabase/client.ts 里的一行createBrowserClient。接下来,登录与注册就变成了两个纯粹的 Server Action——见 app/login/actions.ts:
'use server' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation' import { createClient } from '@/lib/supabase/server' export async function login(formData: FormData) { const supabase = await createClient() const data = { email: formData.get('email') as string, password: formData.get('password') as string, } const { error } = await supabase.auth.signInWithPassword(data) if (error) { redirect('/error') } revalidatePath('/', 'layout') redirect('/account') } export async function signup(formData: FormData) { const supabase = await createClient() const data = { email: formData.get('email') as string, password: formData.get('password') as string, } const { error } = await supabase.auth.signUp(data) // ... }注意两个细节。第一,登录页本身就是一个普通表单,靠formAction={login}和formAction={signup}区分两个提交目标,全程没有一行手写 fetch。第二,如果开启了邮件确认(默认在 supabase/config.toml 中enable_confirmations = true),注册后用户会收到一封带 token 的邮件,点击后由 app/auth/confirm/route.ts 接收并调用supabase.auth.verifyOtp()完成验证——这个"验证-换 session-重定向"的闭环同样是官方模板铺好的。
至于登录态的全局同步,examples/realtime/nextjs-auth-presence/lib/supabase-context.tsx 给出了一个极简模式:用onAuthStateChange订阅事件流,SIGNED_IN进聊天室、SIGNED_OUT踢回登录页,用户对象永远与真实 session 保持一致。五分钟左右,注册、登录、邮件验证、登出全齐——这就是"BaaS 吃掉鉴权样板代码"的直观体验。
Postgres + 实时订阅:消息广播的两种姿势
登录只是入场券,聊天室的核心是"一条消息发出去,所有在线的人立刻看到"。这一层在传统架构里通常意味着 WebSocket 网关、连接管理、消息队列——而在 Supabase 里,它就是Postgres 数据库自身的变更流。
官方的 Slack 克隆示例 examples/slack-clone/nextjs-slack-clone/full-schema.sql 展示了最经典的姿势:先建好channels、messages、users三张表,然后把它们加入实时发布(publication):
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;前端这边,客户端代码在 lib/Store.js 中通过postgres_changes订阅表的 INSERT / DELETE 事件,payload 里的new行就是新消息,直接推进 React state 即可:
const messageListener = supabase .channel('public:messages') .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, (payload) => handleNewMessage(payload.new) ) .subscribe()但如果你读得更深一层,会发现官方在 examples/prompts/use-realtime.md(一份专为 AI 助手编写的 Realtime 实现规范)里给出了更现代的建议:新项目优先用broadcast而不是postgres_changes。理由是 postgres_changes 是单线程轮询,扩展性受限;而broadcast直接走 WebSocket,吞吐和延迟控制都更好。官方甚至给了一张决策表:表变更通知用"数据库触发器 + broadcast",客户端到客户端的即时消息直接用纯 WebSocket 的 broadcast。
这一姿势的最佳实战是 examples/realtime/nextjs-authorization-demo——一个名为 SupaSecureSlack 的授权聊天室。它的核心在 app/protected/page.tsx:进入房间前先用supabase.realtime.setAuth(token)把用户 JWT 注入实时连接,然后创建私有频道,同时监听 broadcast(聊天消息)和 presence(在线成员):
let newChannel = supabase.channel(selectedRoom, { config: { broadcast: { self: true }, private: true, // 私有频道:需要认证 + RLS 授权 }, }) newChannel .on('broadcast', { event: 'message' }, ({ payload }) => addMessage(payload.user_id == user?.id, false, payload.message) ) .on('presence', { event: 'join' }, ({ newPresences }) => { newPresences.map(({ email }) => users.add(email)) setUsers(new Set(users)) }) .subscribe((status, err) => { if (status == 'SUBSCRIBED') { newChannel.track({ email: user?.email }) // 上报自己的在线状态 } })发消息则通过channel.send({ type: 'broadcast', event: 'message', payload })一行搞定。这份规范还给出了频道命名的硬约束:scope:entity:id(如room:123:messages)、事件名用entity_action(如message_created),并在所有实现里强制包含退订清理逻辑——这些约定对 AI 生成的代码尤其重要,因为它们能让"自动写的实时逻辑"和"人写的实时逻辑"长得一样可维护。
行级安全策略:给 AI 聊天数据上锁
如果说前两节解决的是"功能",这一节解决的是"责任"。回到开头那个扎眼的数据:1.6 万个暴露的 Supabase 数据库。它们的共性问题几乎都是同一个——表建好了、RLS 没开,或者开了 RLS 但 anon key(匿名 key)被赋予了过宽的权限。Supabase 的匿名 key 是可以安全放在前端的,但它能读到什么、写到什么,完全由 Postgres 的 RLS 策略说了算。这条安全边界,就是聊天室和"裸奔数据库"之间的分界线。
RLS 的原理非常优雅:每个登录用户会被签发一个 JWT,其中携带role = 'authenticated'和sub(即auth.uid())。数据库在执行每一条 SQL 前,都会用策略里的表达式判断"当前用户能不能看到/写入这一行"。官方用户管理示例的迁移文件 supabase/migrations/20221017024722_init.sql 是教科书级的写法:
alter table profiles enable row level security; create policy "Public profiles are viewable by everyone." on profiles for select using (true); create policy "Users can insert their own profile." on profiles for insert with check (auth.uid() = id); create policy "Users can update own profile." on profiles for update using (auth.uid() = id);这段 SQL 的精髓是最后一行的auth.uid() = id:任何人都看得到资料,但只有 id 等于自己的用户才能改自己的资料。同一份迁移里还配套了一个handle_new_user()触发器,在auth.users插入后自动为新人建好 profile 行——用户体系与业务表之间的"胶水"也由数据库自己完成。
放到聊天场景,Slack 克隆的 full-schema.sql 把这条思路推演到了角色权限层:消息只允许作者本人更新/删除,频道只允许创建者删除,再配合custom_access_token_hook把管理员角色写进 JWT claim,实现authorize('messages.delete')这种函数式权限判断。而 SupaSecureSlack 更进一步,把 RLS 延伸到了实时连接本身——私有频道的授权不再由客户端"自觉"决定,而是由数据库策略裁决。它针对realtime.messages表(Realtime 内部用来转发广播消息的表)建立策略:只有当rooms_users表里存在"当前用户 + 当前频道名"的组合记录时,才允许 SELECT(读消息)和 INSERT(发消息):
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') ) );这套设计的巧妙之处在于:RLS 不是只拦数据库查询,它还拦住了 Realtime 的 WebSocket 流量。即便有人拿到前端的 anon key 强行构造一个私有频道订阅,也会被realtime.topic()与auth.uid()的组合检查拒之门外。官方在 README 中贴出的对比截图非常直观:被授权用户正常进入聊天界面,而 RLS 判定无权访问的用户会看到明确的拒绝状态——这正是"数据安全由数据库兜底,而非由前端自觉"的落地形态。
结语:Vibe Coding 的第一课,是学会"兜底"
把三段代码串起来看,Vibe Coding 时代的第一课其实不是"更快地写出代码",而是理解 AI 替你省略的那一层基础设施。Supabase 把鉴权、实时、权限这三块最难做对的后端能力压缩成了"一段 SQL + 两个客户端方法",让 AI 生成的 Next.js 前端可以在一个下午内长出一个可上线的聊天室——这是它成为 vibe coding 默认后端的原因。但同一枚硬币的另一面是:当安全边界从"自己写的中间件"变成了"数据库里的一个布尔开关"时,懂不懂 RLS 的差别,就是"1.6 万个暴露数据库"和"私有频道严格授权"的差别。
所以,试着打开 examples/realtime/nextjs-authorization-demo 跑一遍:注册一个账号,创建一个房间,再开一个无权限的账号看看被拒的界面。你会同时体会到 BaaS 的效率红利和 Postgres 权限模型的严谨——这两者合起来,才是"AI 应用工程师"真正该学的第一课。
【免费下载链接】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),仅供参考