Search code examples
typescriptnext.jssupabasezustand

"No storage option exists to persist the session" error when using Supabase and Zustand in a Next.js application with Clerk.dev authentication?


I have set up a Next.js application with authentication handled by Clerk.dev and data storage managed by Supabase. I am also using Zustand for state management. However, I am encountering an error that says "No storage option exists to persist the session, which may result in unexpected behavior when using auth."

    "@clerk/nextjs": "^4.16.4",
    "next": "^12.0.9",
    "react": "17.0.2",
    "zustand": "^4.3.8"

Here is a simplified version of my code:

File: utils\supabase.ts

import { createClient, SupabaseClient } from '@supabase/supabase-js';

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

export const supabase: SupabaseClient = createClient(supabaseUrl, supabaseKey);

File: store\useUserStore.ts

import { create } from 'zustand';
import { supabase } from '../utils/supabase';

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: (userId: string) => Promise<void>;
    createUser: (userId: string, user: Partial<User>) => Promise<void>;
    updateUser: (userId: string, updates: Partial<User>) => Promise<void>;
    deleteUser: (userId: string) => Promise<void>;
};

export const useUserStore = create<UserStore>((set) => ({
    userData: null,
    status: 'idle',
    error: null,
    fetchUser: async (userId) => {
        try {
            set({ status: 'loading' });
            const { data, error } = await supabase
            .from('users')
            .select()
            .eq('id', userId)
            if (error) {
                throw new Error('Error fetching user');
            }
            set({ userData: data![0] ?? null, status: 'idle', error: null });
        } catch (error) {
            set({ status: 'error', error: error.message });
        }
    },
    createUser: async (userId, user) => {
       // code here
    },
    updateUser: async (userId, updates) => {
        // code here
    },
    deleteUser: async (userId) => {
        // code here
    },
}));

The error message states that no storage option exists to persist the session, which could lead to unexpected behavior when using authentication. I want to enable the persistSession option but I'm unsure how to proceed.

For additional context, I am using Clerk.dev for authentication, connecting it to Supabase using JWT. I have also set up a Row-Level Security (RLS) policy for the SELECT operation, targeting all (public) roles and using a WITH CHECK expression.

I would greatly appreciate any guidance or suggestions on how to resolve this error and properly configure the session storage to work seamlessly with Clerk.dev, Supabase, and Zustand. Thank you!

Back then, my code was structured like this:

import { createClient } from '@supabase/supabase-js'
import { useSession } from '@clerk/nextjs'
/// .....
const supabaseClient = async supabaseAccessToken => {
    const supabase = createClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL,
        process.env.NEXT_PUBLIC_SUPABASE_KEY,
        {
            global: { headers: { Authorization: `Bearer ${supabaseAccessToken}` } }
        }
    )
    return supabase
}
/// .....
export default function Messages() {
//...
const { session } = useSession()
//...
const supabaseAccessToken = await session.getToken({
    template: 'Supabase'
})
const supabase = await supabaseClient(supabaseAccessToken)
const { data } = await supabase
    .from('user')
    .insert(
        {
            id: session.user.id,
            email: session.user?.primaryEmailAddress?.emailAddress,
        }
    )
    .select()
setUserSettings(data[0])
//...

This code looks messy because I'm doing it directly in a .tsx file. I want to use state management (Zustand) to improve the code structure and adhere to good practices.


Solution

  • I was able to achieve this without using local storage, as recommended by other people in the Discord community.

    Initially, I had to find an alternative solution. So, what I did was incorporate session management into state management. In the client-side component, here's my code:

    import {
      useSession,
      //....
    } from '@clerk/nextjs'
    import React, { useState, useEffect } from 'react';
    
    interface ContentsProps {
      //...
    }
    
    const Contents = (props: ContentsProps) => {
    
      const { session } = useSession()
      const { userData, status, error, fetchUser } = useUserStore();
      //...
      useEffect(() => {
        if (session) {
          fetchUser(session.user.id, session);
        }
      }, []);
    
      return (
      //...
      );
    };
    
    export default Contents;
    

    As a result, I no longer have the ./utils/supabase.ts file. I directly inserted or included it in the store. Therefore, this is my updated ./store/useUserStore.ts file:

    import { create } from 'zustand';
    import { createClient } from '@supabase/supabase-js'
    
    const supabaseClient = async supabaseAccessToken => {
        const supabase = createClient(
            process.env.NEXT_PUBLIC_SUPABASE_URL,
            process.env.NEXT_PUBLIC_SUPABASE_KEY,
            {
                global: { headers: { Authorization: `Bearer ${supabaseAccessToken}` } }
            }
        )
        return supabase
    }
    
    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: (userId: string, session: any) => Promise<void>;
        //....
    };
    
    export const useUserStore = create<UserStore>((set) => ({
        userData: null,
        status: 'idle',
        error: null,
        fetchUser: async (userId, session) => {
            try {
            set({ status: 'loading' });
            const supabaseAccessToken = await session.getToken({
                template: 'Supabase'
            })
            const supabase = await supabaseClient(supabaseAccessToken)
            const { data, error } = await supabase
                .from('users')
                .select()
                .eq('id', userId)
                console.log(data)
            if (error) {
                console.error(error)
                throw new Error('Error fetching user');
            }
            set({ userData: data![0] ?? null, status: 'idle', error: null });
            } catch (error) {
            set({ status: 'error', error: error.message });
            }
        },
        //....
    }));
    

    I hope this enhanced explanation clarifies the changes I made and provides a clearer understanding of the code."