如何在Next.js中创建可共享的Supabase服务端客户端
在Next.js中全局复用Supabase服务端客户端
首先纠正一个误解:createServerClient本身不是单例,每次调用都会生成新的客户端实例。服务端必须为每个请求维护独立的客户端(因为每个请求的用户认证cookie不同),直接全局共享单例会导致请求间的上下文污染。但我们可以通过请求级别的缓存来避免同一请求内重复创建客户端,同时保证请求间的隔离。
实现方案:用AsyncLocalStorage做请求上下文隔离
Node.js的AsyncLocalStorage可以存储请求级别的上下文,确保每个请求有独立的Supabase客户端,同时同一请求内多次调用可复用实例。
1. 创建Supabase客户端工具文件
新建lib/supabase/server.ts,实现请求级别的客户端缓存:
import { createServerClient, type CookieOptions } from '@supabase/ssr' import { cookies } from 'next/headers' import { AsyncLocalStorage } from 'async_hooks' // Pages Router需要额外导入cookie库:import cookie from 'cookie' // 初始化请求上下文存储 const supabaseContext = new AsyncLocalStorage<ReturnType<typeof createServerClient>>() // App Router/Next.js Actions 通用方法 export function getSupabaseServerClient() { // 检查当前请求上下文是否已有客户端,有则直接返回 const existingClient = supabaseContext.getStore() if (existingClient) { return existingClient } const cookieStore = cookies() const newClient = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, { cookies: { get(name: string) { return cookieStore.get(name)?.value }, set(name: string, value: string, options: CookieOptions) { try { cookieStore.set({ name, value, ...options }) } catch (err) { // 服务器组件中无法设置cookie,忽略错误避免崩溃 } }, remove(name: string, options: CookieOptions) { try { cookieStore.set({ name, value: '', ...options, maxAge: 0 }) } catch (err) { // 同上处理 } }, }, } ) // 将新客户端存入当前请求上下文 supabaseContext.run(newClient, () => {}) return newClient } // Pages Router API路由适配方法(可选) export function getSupabaseServerClientForPages(req: NextApiRequest, res: NextApiResponse) { const existingClient = supabaseContext.getStore() if (existingClient) { return existingClient } const newClient = createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, { cookies: { get(name: string) { return req.cookies[name] }, set(name: string, value: string, options: CookieOptions) { res.setHeader('Set-Cookie', cookie.serialize(name, value, options)) }, remove(name: string, options: CookieOptions) { res.setHeader('Set-Cookie', cookie.serialize(name, '', { ...options, maxAge: 0 })) }, }, } ) supabaseContext.run(newClient, () => {}) return newClient }
2. 在Next.js Actions中使用
// app/actions.ts 'use server' import { getSupabaseServerClient } from '@/lib/supabase/server' export async function fetchUser(userId: string) { const supabase = getSupabaseServerClient() // 同一Action内多次调用会复用同一个客户端实例 const { data, error } = await supabase.from('users').select('*').eq('id', userId).single() if (error) throw new Error(error.message) return data }
3. 在Pages Router API路由中使用
// pages/api/users.ts import type { NextApiRequest, NextApiResponse } from 'next' import { getSupabaseServerClientForPages } from '@/lib/supabase/server' export default async function handler(req: NextApiRequest, res: NextApiResponse) { const supabase = getSupabaseServerClientForPages(req, res) const { data, error } = await supabase.from('users').select('*') if (error) return res.status(500).json({ error: error.message }) res.status(200).json(data) }
关键注意事项
- 请求隔离:
AsyncLocalStorage确保每个请求的客户端独立,不会出现不同用户的认证上下文混淆。 - 权限安全:
SUPABASE_SERVICE_ROLE_KEY拥有Supabase的最高权限,务必通过环境变量配置,绝对不能暴露给客户端代码。 - 开发环境兼容:Next.js热重载不会破坏
AsyncLocalStorage的隔离性,每个请求仍会维护独立的客户端实例。 - 服务器组件限制:在服务器组件中调用
set/removecookie会报错,所以代码中加入了错误捕获,避免应用崩溃。
内容的提问来源于stack exchange,提问作者insivika
相关产品推荐
相关产品推荐

