supabase icon indicating copy to clipboard operation
supabase copied to clipboard

Possible race condition

Open miguel-arrf opened this issue 2 months ago • 2 comments

Hi!

I have the following structure:

  • a login page
  • a dashboard page (for signed in users)
  • an auth middleware (not global, and both pages are using it via the definePageMeta)

What's happening:

  1. I log in using the signInWithPassword. I await that call, and after it is completed and without errors I redirect the user to the dashboard page.
  2. The auth middleware was triggered, allowed the passage to the dashboard page since for the middleware the session (from useSupabaseSession) is valid (as expected).
  3. The dashboard page loads. I then have this (more or less, this is a reduced example):
<script setup lang="ts">
const user = useSupabaseUser()
onMounted(async () => {
    if (!user.value || !user.value.email) {
        console.error("no user!")
    }
})
</script>

Problem: The user is not defined during the onMounted and I don't get why. If I reload the page, it works. If I await a bit before redirecting the page, it also works, but that seems rather inelegant.

Thank you for the help!

miguel-arrf avatar Nov 16 '25 21:11 miguel-arrf

If my understanding is correct, this should be handled with a /confirm page (as shown here: https://supabase.nuxtjs.org/getting-started/authentication).

Would it be reasonable to expect that after awaiting signInWithPassword, the client-side useSupabaseUser would already be updated?

miguel-arrf avatar Nov 16 '25 23:11 miguel-arrf

If my understanding is correct, this should be handled with a /confirm page (as shown here: https://supabase.nuxtjs.org/getting-started/authentication).

This is ideal or using a plugin that listens for an auth state change and triggers an action.

This is mine based off the docs.

// plugins/supabase-auth-listener.client.ts
export default defineNuxtPlugin(() => {
  const supa = useSupabaseClient();
  const { refreshProfile, logout, doLocalLogout } = useAuth();
  const teams = useTeamsStore();

  const subKey = '__supabase_auth_sub__';

  // Cleanup for HMR
  if (import.meta.hot && (window as any)[subKey]?.unsubscribe) {
    (window as any)[subKey].unsubscribe();
  }

  const {
    data: { subscription },
  } = supa.auth.onAuthStateChange(async (event) => {
    switch (event) {
      case 'SIGNED_IN':
      case 'TOKEN_REFRESHED':
      case 'USER_UPDATED': {
        await refreshProfile(true);
        await teams.initialize(useAuth().profile.value);
        break;
      }
      case 'PASSWORD_RECOVERY':
      case 'INITIAL_SESSION': {
        await refreshProfile(true);
        await teams.initialize(useAuth().profile.value);
        break;
      }
      case 'SIGNED_OUT': {
        await doLocalLogout();
        break;
      }
      default:
        break;
    }
  });

  (window as any)[subKey] = subscription;

  if (import.meta.hot) {
    import.meta.hot.dispose(() => subscription.unsubscribe());
  }

  // Cross-tab sign-outs / sign-ins
  window.addEventListener('storage', async (e) => {
    if (e.key?.includes('-auth-token')) {
      const {
        data: { session },
      } = await supa.auth.getSession();
      if (!session) {
        await logout();
      } else {
        await refreshProfile(true);
        await teams.initialize(useAuth().profile.value);
      }
    }
  });
});

Matthewenderle avatar Nov 22 '25 00:11 Matthewenderle