# 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.

Source: https://supahost.app/kb/nextjs-auth-troubleshooting · Updated 2026-11-11

## 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](/kb/auth-helpers-to-ssr).

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`:

   ```ts
   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](/docs/auth/nextjs) 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](/kb/oauth-server-side-redirect).

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**.

## Related

- [Auth in Next.js (App Router)](/docs/auth/nextjs)
- [Server-side auth and SSR](/docs/auth/server-side)
- [PKCE errors: code verifier, cannot parse response](/kb/pkce-flow-errors)
- [Can I use SupaHost with a Next.js app on Vercel?](/kb/nextjs-vercel)
