You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js集成Clerk、Supabase与Zustand时会话持久化错误排查

1. 解决Supabase会话持久化错误

错误根源是Supabase默认启用persistSession自动会话持久化,但你的架构中会话由Clerk管理,无需Supabase自行处理。同时需确保Supabase请求携带Clerk生成的Supabase JWT,才能通过RLS策略校验。

修改utils/supabase.ts,创建动态带权的Supabase客户端生成函数,同时禁用Supabase的会话持久化:

import { createClient, SupabaseClient } from '@supabase/supabase-js';
import { getAuth } from '@clerk/nextjs/server';
import { useSession } from '@clerk/nextjs';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_KEY!;

// 服务器端用:获取带Clerk认证的Supabase客户端
export const createServerSupabaseClient = async (req: any, res: any): Promise<SupabaseClient> => {
  const { session } = await getAuth(req, res);
  const supabaseAccessToken = session?.getToken({ template: 'Supabase' });

  return createClient(supabaseUrl, supabaseKey, {
    global: {
      headers: {
        Authorization: supabaseAccessToken ? `Bearer ${supabaseAccessToken}` : '',
      },
    },
    auth: {
      persistSession: false, // 禁用Supabase会话持久化,交由Clerk管理
    },
  });
};

// 客户端用:获取带Clerk认证的Supabase客户端
export const useClientSupabaseClient = () => {
  const { session } = useSession();

  return createClient(supabaseUrl, supabaseKey, {
    global: {
      headers: {
        Authorization: session ? `Bearer ${session.getToken({ template: 'Supabase' })}` : '',
      },
    },
    auth: {
      persistSession: false,
    },
  });
};

2. 优化Zustand Store结构

替换原无认证的Supabase客户端,改为动态获取带权客户端,同时简化参数传递(直接从Clerk会话取用户ID):

修改store/useUserStore.ts:

import { create } from 'zustand';
import { useClientSupabaseClient } from '../utils/supabase';
import { useSession } from '@clerk/nextjs';

type User = {
  id: string;
  email: string;
  custom_api: string;
  bot_type: string;
  discord_id: string;
};

type UserStore = {
  userData: User | null;
  status: 'idle' | 'loading' | 'error';
  error: string | null;
  fetchUser: () => Promise<void>;
  createUser: (user: Partial<User>) => Promise<void>;
  updateUser: (updates: Partial<User>) => Promise<void>;
  deleteUser: () => Promise<void>;
};

export const useUserStore = create<UserStore>((set) => ({
  userData: null,
  status: 'idle',
  error: null,
  fetchUser: async () => {
    try {
      set({ status: 'loading' });
      const supabase = useClientSupabaseClient();
      const { session } = useSession();
      if (!session) throw new Error('未登录');

      const { data, error } = await supabase
        .from('users')
        .select()
        .eq('id', session.user.id)
        .single();

      if (error) throw error;
      set({ userData: data, status: 'idle', error: null });
    } catch (error: any) {
      set({ status: 'error', error: error.message });
    }
  },
  createUser: async (user) => {
    try {
      set({ status: 'loading' });
      const supabase = useClientSupabaseClient();
      const { session } = useSession();
      if (!session) throw new Error('未登录');

      const { data, error } = await supabase
        .from('users')
        .insert({ 
          id: session.user.id, 
          email: session.user.primaryEmailAddress?.emailAddress, 
          ...user 
        })
        .select()
        .single();

      if (error) throw error;
      set({ userData: data, status: 'idle', error: null });
    } catch (error: any) {
      set({ status: 'error', error: error.message });
    }
  },
  updateUser: async (updates) => {
    try {
      set({ status: 'loading' });
      const supabase = useClientSupabaseClient();
      const { session } = useSession();
      if (!session) throw new Error('未登录');

      const { data, error } = await supabase
        .from('users')
        .update(updates)
        .eq('id', session.user.id)
        .select()
        .single();

      if (error) throw error;
      set({ userData: data, status: 'idle', error: null });
    } catch (error: any) {
      set({ status: 'error', error: error.message });
    }
  },
  deleteUser: async () => {
    try {
      set({ status: 'loading' });
      const supabase = useClientSupabaseClient();
      const { session } = useSession();
      if (!session) throw new Error('未登录');

      const { error } = await supabase.from('users').delete().eq('id', session.user.id);
      if (error) throw error;

      set({ userData: null, status: 'idle', error: null });
    } catch (error: any) {
      set({ status: 'error', error: error.message });
    }
  },
}));

关键优化点

  • 无需手动传userId,直接从Clerk会话获取当前用户ID,减少冗余参数
  • 所有Supabase请求携带Clerk JWT,符合RLS策略校验要求
  • 用single()替代data![0],语义更明确且避免空值风险
  • 统一错误处理逻辑,直接抛出Supabase原生错误便于排查

3. 组件中使用优化后的Store

简化Messages组件逻辑:

import { useUserStore } from '../store/useUserStore';
import { useSession } from '@clerk/nextjs';
import { useEffect } from 'react';

export default function Messages() {
  const { session } = useSession();
  const { userData, createUser, status, error } = useUserStore();

  useEffect(() => {
    if (session && !userData) {
      // 已登录但无用户数据时自动创建/获取
      createUser({});
    }
  }, [session, userData, createUser]);

  if (status === 'loading') return <div>加载中...</div>;
  if (error) return <div>错误:{error}</div>;

  return (
    <div>
      {userData ? (
        <div>欢迎,{userData.email}</div>
      ) : (
        <div>未获取到用户信息</div>
      )}
    </div>
  );
}

4. 补充配置说明

  • 确保Clerk后台已配置Supabase JWT模板,且模板受众(aud)设置为Supabase项目的引用ID
  • 建议优化RLS策略,明确限定用户仅能访问自身数据:
    CREATE POLICY "用户仅能查看自身数据" ON public.users
    FOR SELECT USING (auth.uid() = id);
    
  • SSR场景下使用createServerSupabaseClient获取客户端,避免在服务器端调用useSession

内容的提问来源于stack exchange,提问作者user20951184

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 12:22:05