OAuth sign-in not redirecting on the server
signInWithOAuth in a server action or route does nothing, or the user comes back signed out. On the server you must redirect to data.url yourself and exchange the code in a callback route.
What you see
- A server action or API route calls
supabase.auth.signInWithOAuth()and nothing happens. There's no error and no redirect to GitHub or Google. - Or the provider screen appears, but the user returns to
/auth/callback?code=...and is still signed out. - Or the callback fails with
invalid request: both auth code and code verifier should be non-empty.
Why it happens
In the browser, signInWithOAuth() sends the window to the provider for you. On the server there's no window, so the method only returns the provider URL in data.url. Your code has to send the redirect.
Server-side sign-in also uses the PKCE flow. Auth comes back to your app with a code, and your callback must swap it for a session with exchangeCodeForSession(). That swap needs the code verifier the server client saved in a cookie when sign-in started. If the client can't write cookies, or nothing handles /auth/callback, the user never gets a session.
How to fix it
These steps use the Next.js App Router with @supabase/ssr. The server client in lib/supabase/server.ts must implement both getAll and setAll for cookies; see Auth in Next.js.
Redirect to
data.urlin the server action:// app/login/actions.ts "use server"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; import { createClient } from "@/lib/supabase/server"; export async function signInWithGitHub() { const supabase = await createClient(); const origin = (await headers()).get("origin"); const { data, error } = await supabase.auth.signInWithOAuth({ provider: "github", options: { redirectTo: `${origin}/auth/callback` }, }); if (error) redirect("/login?error=oauth"); redirect(data.url); }Don't put
redirect()inside atry/catch. Next.js implements it by throwing, and the catch swallows the redirect.Exchange the code in a callback route:
// app/auth/callback/route.ts import { NextResponse, type NextRequest } from "next/server"; import { createClient } from "@/lib/supabase/server"; export async function GET(request: NextRequest) { const { searchParams, origin } = new URL(request.url); const code = searchParams.get("code"); if (code) { const supabase = await createClient(); const { error } = await supabase.auth.exchangeCodeForSession(code); if (!error) return NextResponse.redirect(`${origin}/team`); } return NextResponse.redirect(`${origin}/login?error=callback`); }Allow the callback URL. Add
http://localhost:3000/**and your production address (for examplehttps://chat.example.com/**) under Dashboard → your project → Auth tab → Redirect URLs, then click Save Auth settings. Without this, Auth ignoresredirectToand sends users to the Site URL.Start and finish in the same browser. The code verifier cookie lives in the browser that started sign-in. Test in a normal tab, not one browser for the form and another for the callback.
On SupaHost
- There are two different callback URLs. The provider (GitHub, Google and so on) redirects to your project's Auth:
https://app-one.acme.supahost.space/auth/v1/callback, which you register in the provider's console. Auth then redirects to your app's/auth/callback, which goes in Redirect URLs. - Once a custom domain is active on the project, the provider callback uses that domain instead. Update the provider's console when you add one.