每个SaaS都要搭同一套东西:登录、支付、数据库、权限路由。我从零建过太多次。这篇不讲理想情况,只讲那些会让生产环境崩掉的边缘案例。

三个Supabase客户端的问题

打开网易新闻 查看精彩图片

大多数教程只展示一个客户端。Next.js 15 App Router项目里你需要三个:

打开网易新闻 查看精彩图片

第一个是浏览器端专用,跑在Client Components里,用createBrowserClient创建,负责客户端的认证状态。

第二个给Server Components和API路由,用createServerClient。它从Next.js请求上下文读取cookie,能刷新会话。关键是setAll回调——得把cookie写回响应,不然会话状态不同步。

第三个最特殊:service角色客户端,webhook专用。它用SUPABASE_SERVICE_ROLE_KEY,完全绕过行级安全(RLS)。webhook里没有已登录用户,必须用这个才能读写数据库。

用错场景的bug很隐蔽:会话不持久、webhook报RLS错误、登录后状态不更新。

能真正刷新会话的中间件

路由保护简单,会话刷新才是坑。中间件里必须调用supabase.auth.getUser(),这行代码会触发令牌刷新。很多人抄了中间件模板却漏掉这行,结果用户一小时后被踢出登录。

cookie处理也有讲究:先set到request,再复制到response。顺序错了,Next.js会忽略。

Stripe订阅的webhook坑

打开网易新闻 查看精彩图片

checkout.session.completed事件到达时,你的数据库用户可能还不存在。用户刚付款,Supabase用户表还没写入。直接关联会报错。

解法:webhook里先upsert用户,再写订阅记录。用service客户端,关掉RLS。别在webhook里做耗时操作,Stripe 5秒超时很严格。

价格ID别硬编码。Stripe推荐用查找键(lookup_key),这样换价格方案不用改代码。环境变量里存键名,运行时查实时价格ID。

权限路由的隐藏依赖

Dashboard页面常用Server Component查用户数据。但中间件刚刷新过会话,Server Component里又调getUser(),两次数据库往返浪费资源。

更好的做法:中间件把user塞进header,Server Component读header。省一次查询,延迟降30-50ms。Next.js 15的unstable_after也能用,但header方案更可控。

本地开发时,Stripe CLI的forward经常漏事件。webhook日志里看不到,数据库也没记录,以为是代码bug。其实是CLI没连上。加条健康检查路由,ping一下Stripe API确认连通性。

生产环境记得给webhook验签。raw body得在middleware里保留,Next.js默认会parse。用export const config = { api: { bodyParser: false } }关掉,或者换App Router的route.ts,直接读request.text()。