Skip to content

Supabase Auth errorSupabase: “invalid flow state, no valid flow state found” and “both auth code and code verifier should be non-empty”

A user clicks a sign-in, magic link or password reset link, lands on your callback, and exchangeCodeForSession fails with flow_state_not_found, bad_code_verifier or “both auth code and code verifier should be non-empty”. These are PKCE errors: the code in the URL can’t be matched to the code verifier the browser saved when the flow started.

Short answer

Exchange the code once, in the same browser that started the sign-in, within 5 minutes. In Next.js and other SSR apps, create the client with @supabase/ssr so the code verifier is stored in a cookie the callback route can read. For email links that users open on another device, use a token_hash link with verifyOtp instead of a PKCE code.

Why the PKCE exchange fails, and the fix

  1. 01

    The link was opened in a different browser or device

    “That means the code exchange must be initiated on the same browser and device where the flow was started.” Supabase, PKCE flow (Limitations)

    The code verifier is saved in the browser that started the flow. An email link opened in the mail app’s browser, or on a phone, has the code but no verifier. For email confirmations and password resets, build the link with the token hash and call verifyOtp on your confirm route; it doesn’t need the verifier. Use type recovery for password reset links. Also check that the redirect URL is in the Auth redirect allow-list, with the same www or non-www host, so the callback runs on the origin that holds the cookie.

    // email template link: {{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email
    const { error } = await supabase.auth.verifyOtp({ token_hash, type: 'email' });
  2. 02

    The verifier is in localStorage, the exchange runs on the server

    “The code verifier is created and stored locally when the Auth flow is first initiated.” Supabase, PKCE flow (Limitations)

    A browser client made with createClient from supabase-js keeps the verifier in localStorage, and a server has no localStorage. A route handler that exchanges the code finds no verifier and sends an empty one. Use createBrowserClient and createServerClient from @supabase/ssr, which keep it in cookies.

    // app/auth/callback/route.ts
    const code = new URL(request.url).searchParams.get('code');
    if (code) {
      const supabase = await createClient(); // createServerClient from @supabase/ssr
      const { error } = await supabase.auth.exchangeCodeForSession(code);
    }
  3. 03

    The code was used twice or expired

    “PKCE flow state to which the API request relates no longer exists. Flow states expire after a while and are progressively cleaned up, which can cause this error.” Supabase, Error codes (flow_state_not_found)

    React strict mode, a double render or the user pressing back can run the exchange a second time, and the first run already consumed the flow state. In a browser app, the client already exchanges the code on load (detectSessionInUrl is on by default), so a manual exchangeCodeForSession on the same URL is a second exchange: remove one. Exchange the code in one place only, and send the user to sign in again if it is older than 5 minutes.

  4. 04

    Two sign-ins started in the same browser

    “the code verifier stored for the earlier flow is overwritten by the later one, and exchanging the first flow's code fails.” Supabase, PKCE flow (Overlapping flows)

    Starting OAuth in two tabs overwrites the first verifier, so the first tab’s code fails to exchange (usually bad_code_verifier or flow_state_not_found). Let only one sign-in run at a time, or disable the button after the first click.

Checked against the vendor’s docs on : Supabase docs: PKCE flow; Supabase docs: Auth error codes; Supabase docs: Creating a Supabase client for SSR.

Rather hand it over?

Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.

Related: Supabase email link invalid or expired · Supabase “Auth session missing” · Supabase auth email links point to localhost · Supabase JWSError JWSInvalidSignature

Questions

More on this error

How long is the PKCE code valid?

Supabase’s docs say the code is valid for 5 minutes and can be exchanged once. After that, the user has to start the sign-in again.

Should I switch back to the implicit flow?

Not for server-side apps: with SSR you are using PKCE, and the implicit flow puts tokens in the URL fragment the server can’t read. Fix where the verifier is stored, or use token_hash links for email.

Can you fix it for me?

If one bug is the whole problem, the App Fix Ticket covers it: a written diagnosis first, then the fix. If the app has more going on, the Launch Check is a written check of the whole app. Each issue found is rated critical, high, medium or low, with where it is and how it was confirmed. No issues worth fixing, no charge. If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way. This applies to the first 10 Launch Checks bought after October 30, 2026.

A different error?

Describe it and get a written quote

Send the error and what you were doing. You get a written scope, a fixed price and a timeline; Simon replies by email, usually within one business day.