Supabase magic-link auth setSession hangs silently in Expo dev client (implicit flow) — no observable error or completion

6 hours ago 1
ARTICLE AD BOX

I have an Expo SDK 54 app using Supabase Auth with magic-link sign-in via implicit flow. When the user taps a magic link, my deep-link callback receives the URL, parses the fragment correctly, and calls supabase.auth.setSession() — but the call hangs silently. Never resolves, never throws. App stays stuck on a "Setting up your account" loading screen indefinitely.

Previously hit the same hang at exchangeCodeForSession with PKCE flow, which is why I pivoted to implicit. Same hang, different boundary.

Stack

Expo SDK 54

React Native (Hermes engine)

@supabase/supabase-js v2.x (latest)

expo-router for navigation

iOS dev client built via EAS Build (custom URL scheme)

AsyncStorage adapter for session storage

react-native-get-random-values polyfill installed and imported first

Auth flow

Gate → Email Entry → SEND LINK → magic link arrives → tap link → deep link opens app → callback parses #access_token → setSession() → HANG

What works (verified with diagnostic logs)

Validation queries against application tables (Supabase round-trip works fine)

signInWithOtp magic-link generation

Deep link opens app correctly with myapp://auth/callback#access_token=...&refresh_token=...

URL fragment parsing extracts tokens correctly

Error path works perfectly — when token is invalid (otp_expired), callback receives the error in fragment, parses it, displays the error, routes back to email entry. All [callback] logs fire in sequence.

What hangs

setSession({ access_token, refresh_token }) with valid unconsumed tokens

Same pattern previously observed with PKCE exchangeCodeForSession

Both auth functions hang silently when handed valid tokens

Diagnostic logs

When tapping a clean unconsumed magic link (success path):

[callback] run() entered, url: myapp://auth/callback#access_token=eyJ...&refresh_token=...&token_type=bearer&expires_in=3600 [callback] parsed fragment, has tokens: true [callback] setSession start

Then nothing. No setSession done log ever appears.

When tapping a consumed magic link (Gmail pre-fetched it):

[callback] run() entered, url: myapp://auth/callback#error=access_denied&error_code=otp_expired&... [callback] parsed fragment, has tokens: false errorCode: access_denied [callback] error in fragment: access_denied Email link is invalid or has expired [callback] routing to: /(auth)/email (error path)

Full happy path here - error case works perfectly. Only the success case hangs.

Things I've tried

PKCE flow with react-native-get-random-values polyfill (provides crypto.getRandomValues but NOT crypto.subtle)

Pivoted to implicit flow (flowType: 'implicit') to avoid PKCE entirely

Replaced SecureStore + chunking adapter with simple AsyncStorage adapter

Fresh EAS dev client builds (multiple)

Clean app installs (uninstall + reinstall to wipe AsyncStorage)

Multiple email delivery methods to ensure token isn't pre-consumed (temp-mail.org, copying link via Notes)

Supabase client config

lib/supabase.ts

import 'react-native-get-random-values'; import 'react-native-url-polyfill/auto'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { createClient } from '@supabase/supabase-js'; export const supabase = createClient(supabaseUrl, supabaseAnonKey, { auth: { storage: AsyncStorage, autoRefreshToken: true, persistSession: true, detectSessionInUrl: false, // we handle deep link callback ourselves flowType: 'implicit', }, });

Callback handler

app/auth/callback.tsx (relevant excerpt)

const url = Linking.useURL(); useEffect(() => { if (!url) return; if (processedRef.current === url) return; processedRef.current = url; const run = async () => { console.log('[callback] run() entered, url:', url); const fragment = parseAuthFragment(url); console.log('[callback] parsed fragment, has tokens:', !!fragment?.accessToken); if (fragment?.error) { // ... error path works correctly ... return; } if (!fragment?.accessToken || !fragment?.refreshToken) { // ... missing tokens path ... return; } console.log('[callback] setSession start'); const { error } = await supabase.auth.setSession({ access_token: fragment.accessToken, refresh_token: fragment.refreshToken, }); console.log('[callback] setSession done:', error); // ← NEVER PRINTS // ... rest of flow never executes ... }; run(); }, [url]);

Metro warning that may be related

Throughout the app I see this warning fire whenever supabase-js initializes:

WARN WebCrypto API is not supported. Code challenge method will default to use plain instead of sha256.

This warning makes sense for PKCE (which needs crypto.subtle.digest for S256). But after pivoting to implicit flow, the warning still fires. And setSession still hangs. Suggests supabase-js is touching crypto.subtle somewhere even on implicit flow paths — possibly during session initialization, refresh token nonce generation, or internal state ID generation — and silently hanging on the missing API.

Question

Has anyone hit setSession (or exchangeCodeForSession) hanging silently in React Native + Hermes specifically with Supabase magic link auth?

I suspect missing crypto.subtle (WebCrypto's hash/digest APIs aren't polyfilled by react-native-get-random-values — only crypto.getRandomValues).

If you've solved this:

Did adding a fuller WebCrypto polyfill (expo-crypto-as-shim, crypto-browserify, custom shim using expo-crypto's digestStringAsync) help?

Did downgrading @supabase/supabase-js to a specific version work?

Is there a known-good combination of Expo SDK + supabase-js + auth flow type for React Native that doesn't hit this?

Or should I move to email/password auth as a workaround?

I have the architecture working architecturally (error path, fragment parsing, deep linking) — just blocked on this one silent hang for the success path.

Read Entire Article