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.
Use
@supabase/ssreverywhere. UsecreateBrowserClientin Client Components andcreateServerClienton the server. A plain supabase-jscreateClientin 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.Add the proxy. On Next.js 16 it's
proxy.tsat the project root. On 15 and earlier, name the filemiddleware.tsand the functionmiddleware: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.
Wrap
setAllin your server client intry/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 fulllib/supabase/server.ts.Check the user with
getUser()(orgetClaims()) on the server, notgetSession().getSession()returns whatever is in the cookie without checking it.Add a callback route at
app/auth/callback/route.tsthat callsexchangeCodeForSession(code), and pointemailRedirectToandredirectToat it. The link has to be opened in the browser that started sign-in. See OAuth sign-in not redirecting on the server.Allow every URL you deploy to. Add
http://localhost:3000/**, your production domain and your preview pattern (such ashttps://*-team-chat.vercel.app/**) to Redirect URLs.Check the environment variables on the host:
NEXT_PUBLIC_SUPABASE_URLandNEXT_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 likegetUser(). 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-tokenforapp-one.acme.supahost.space. Large sessions are split into.0,.1and so on. - The access token lifetime is set on the Auth tab under Sign-in & security.