Troubleshooting Supabase Auth in Next.js

Fix the common Next.js App Router auth problems with SupaHost. Server components that see no user, sign-outs after an hour, cookie errors, and email links that don't sign users in.

What you see

One or more of these in a Next.js App Router app:

  • The browser shows the user as signed in, but Server Components, Route Handlers or Server Actions see no user.
  • Users are signed out after about an hour.
  • Sign-in works locally but not on the deployed site.
  • Error: Cookies can only be modified in a Server Action or Route Handler.
  • The confirmation email opens your site, but the user isn't signed in.

Why it happens

Server code can only see a session that is stored in cookies, and something has to refresh those cookies when the access token expires. That needs @supabase/ssr on both the browser and the server, a proxy that refreshes the session on every request, and a callback route for links that come back with a code. Missing any one of these gives the symptoms above.

How to fix it

Work through these in order.

  1. Use @supabase/ssr everywhere. Use createBrowserClient in Client Components and createServerClient on the server. A plain supabase-js createClient in the browser keeps the session in local storage, which the server can't read. If you still use @supabase/auth-helpers-nextjs, see Migrating from auth-helpers.

  2. Add the proxy. On Next.js 16 it's proxy.ts at the project root. On 15 and earlier, name the file middleware.ts and the function middleware:

    import { createServerClient } from "@supabase/ssr";
    import { NextResponse, type NextRequest } from "next/server";
    
    export async function proxy(request: NextRequest) {
      let response = NextResponse.next({ request });
    
      const supabase = createServerClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
        {
          cookies: {
            getAll: () => request.cookies.getAll(),
            setAll(cookiesToSet) {
              cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
              response = NextResponse.next({ request });
              cookiesToSet.forEach(({ name, value, options }) =>
                response.cookies.set(name, value, options),
              );
            },
          },
        },
      );
    
      // Refreshes the session if needed and writes the new cookies.
      await supabase.auth.getUser();
      return response;
    }
    
    export const config = {
      matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|svg)$).*)"],
    };
    

    Without it, nothing refreshes the token on the server, and users appear signed out once it expires.

  3. Wrap setAll in your server client in try/catch. Server Components can't write cookies, which causes the "Cookies can only be modified" error. Catching it is safe because the proxy has already refreshed the session. See Auth in Next.js for the full lib/supabase/server.ts.

  4. Check the user with getUser() (or getClaims()) on the server, not getSession(). getSession() returns whatever is in the cookie without checking it.

  5. Add a callback route at app/auth/callback/route.ts that calls exchangeCodeForSession(code), and point emailRedirectTo and redirectTo at it. The link has to be opened in the browser that started sign-in. See OAuth sign-in not redirecting on the server.

  6. Allow every URL you deploy to. Add http://localhost:3000/**, your production domain and your preview pattern (such as https://*-team-chat.vercel.app/**) to Redirect URLs.

  7. Check the environment variables on the host: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY, copied from Dashboard → your project → Connect → App. NEXT_PUBLIC_ values are inlined at build time, so redeploy after changing them.

On SupaHost

  • SupaHost projects use HS256 keys, so getClaims() asks the Auth server to check the token, just like getUser(). Both are safe on the server; neither verifies locally.
  • Session cookies are named after the first part of your project host, for example sb-app-one-auth-token for app-one.acme.supahost.space. Large sessions are split into .0, .1 and so on.
  • The access token lifetime is set on the Auth tab under Sign-in & security.

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us