Add OAuth Login (Google, GitHub) with Supabase Auth
A step-by-step Supabase OAuth tutorial for Next.js: the provider setup, the signInWithOAuth call, the callback route, and the redirect-URL gotchas to avoid.
· Justin Boggs

To add OAuth login with Supabase Auth, you do three things: enable the provider (Google or GitHub) in the Supabase dashboard with a client ID and secret from that provider's developer console, call supabase.auth.signInWithOAuth({ provider, options: { redirectTo } }) from a sign-in button, and create a callback route at app/auth/callback/route.ts that exchanges the returned code for a session. The first time you wire it up, the part that breaks is almost never the code — it's the redirect URLs. Get those right and "Sign in with Google" works in about fifteen minutes. This is the founder-friendly walkthrough of exactly what's happening and where it goes wrong.
TL;DR
- Three moving parts: provider config in Supabase, a
signInWithOAuthcall, and a callback route that runsexchangeCodeForSession.- The callback route lives at
app/auth/callback/route.ts— this is the PKCE flow Supabase recommends for server-side Next.js apps.- Redirect-URL mismatches between Google Cloud, GitHub, and Supabase cause most OAuth failures. Three places have to agree.
- Add
http://localhost:3000for local dev and your production domain separately — and remove localhost before launch.- Google needs an extra
access_type: 'offline'parameter if you ever want a refresh token to call Google APIs later.
Why OAuth instead of email and password
The fastest way to lose a signup is to ask someone to invent and remember a password. OAuth — "Sign in with Google," "Sign in with GitHub" — removes that entire step. The user clicks a button, approves a consent screen they have seen a hundred times, and they are in. No password to forget, no reset-email flow to build, no leaked-credential liability sitting in your database.
For a SaaS aimed at developers, GitHub login is close to mandatory — your users already live there. For everyone else, Google covers the vast majority of accounts. Supporting both, plus a fallback email option, handles almost every visitor without you writing a password-reset flow at all.
Supabase Auth is the piece that makes this manageable for a non-technical founder. It sits between your app and the OAuth providers, handles the token exchange, stores the user, and gives you one consistent user object whether someone signed in with Google, GitHub, or email. If you are still deciding on a database-and-auth layer, the tradeoffs are covered in Supabase vs Firebase for first-time SaaS founders — but if you have already picked Supabase, OAuth is one of the things it does best.
One mental model that helps: Supabase is not doing the authentication. Google and GitHub do that. Supabase is the trusted middleman that asks them "is this person who they say they are?" and, on a yes, mints a session for your app. That is why the setup touches three dashboards — yours, Supabase's, and the provider's. Each one needs to trust the next.
Step 1: Enable the provider in Supabase
Every OAuth provider follows the same shape. You register an application with the provider, the provider hands you a client ID and a client secret, and you paste those into Supabase. Supabase then knows how to talk to that provider on your behalf.
For Google, you create an OAuth client in the Google Auth Platform console and choose Web application as the type, per Supabase's Google login guide. You will be asked for two things that trip people up:
- Authorized JavaScript origins — your app's base URL, like
https://yourapp.comandhttp://localhost:3000during development. - Authorized redirect URIs — your Supabase project's callback URL, not your app's. You copy this exact value from the Google provider page in your Supabase dashboard. It looks like
https://<project-id>.supabase.co/auth/v1/callback.
That distinction is the single most common cause of a broken Google login. The redirect URI Google needs is Supabase's URL, because Google hands the user back to Supabase first, and Supabase then hands them to your app. Two hops, two different URLs.
GitHub is simpler. In your GitHub account, go to Developer Settings, create a new OAuth App, and set the Authorization callback URL to the same Supabase callback URL. GitHub gives you a client ID and secret, and you paste them into the GitHub provider page in Supabase. The Supabase GitHub login guide has the screen-by-screen version.
Once the provider is enabled in Supabase with the client ID and secret saved, the backend half is done. Now the app needs a button.
Step 2: Trigger the login with signInWithOAuth
The client-side call is short. From a sign-in button, you call signInWithOAuth with the provider name and a redirectTo URL that points at your callback route:
'use client'
import { createClient } from '@/utils/supabase/client'
export function OAuthButtons() {
const supabase = createClient()
async function signIn(provider: 'google' | 'github') {
await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
})
}
return (
<div className="flex flex-col gap-2">
<button onClick={() => signIn('google')}>Continue with Google</button>
<button onClick={() => signIn('github')}>Continue with GitHub</button>
</div>
)
}
Using window.location.origin rather than a hardcoded URL means the same code works on localhost:3000 and on your production domain without edits. The browser redirects the user to the provider's consent screen automatically — you do not navigate manually.
When the user approves, the provider sends them back to Supabase, Supabase appends a one-time code to the URL, and the user lands on /auth/callback?code=.... That code is useless on its own; it has to be exchanged for a session. That is what the callback route is for.
A note on flows. Supabase supports an implicit flow (tokens come straight back in the URL) and a PKCE flow (you exchange a code server-side). For a server-rendered Next.js app, Supabase recommends the PKCE flow because the session ends up in secure cookies your server can read. The callback route below is the PKCE handler.
Step 3: Handle the callback and exchange the code
Create a file at app/auth/callback/route.ts. This is a route handler, not a page — it runs on the server, does the code exchange, and redirects. Straight from the Supabase Next.js server-side guide:
import { NextResponse } from 'next/server'
import { createClient } from '@/utils/supabase/server'
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
let next = searchParams.get('next') ?? '/'
if (!next.startsWith('/')) {
next = '/'
}
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
const forwardedHost = request.headers.get('x-forwarded-host')
const isLocalEnv = process.env.NODE_ENV === 'development'
if (isLocalEnv) {
return NextResponse.redirect(`${origin}${next}`)
} else if (forwardedHost) {
return NextResponse.redirect(`https://${forwardedHost}${next}`)
} else {
return NextResponse.redirect(`${origin}${next}`)
}
}
}
return NextResponse.redirect(`${origin}/auth/auth-code-error`)
}
Three things are worth understanding here, because they are the parts you will debug.
The exchangeCodeForSession(code) call is the whole point. It trades the one-time code for a real session and writes it to cookies. After this line succeeds, the user is logged in and any server component can read supabase.auth.getUser().
The next parameter lets you send someone to where they were headed before login. The guard if (!next.startsWith('/')) is a small but real security check — it stops an attacker from crafting a next=https://evil.com link that turns your login into an open redirect. Keep it.
The x-forwarded-host handling exists because hosts like Vercel sit behind a load balancer. In production the original domain arrives in that header, so the redirect goes to your real domain instead of an internal one. In Coding Capybaras this exact handler lives at platform/pages/auth/callback/route.ts — it ships wired up so you do not have to write it from scratch.
If something fails, the user lands on /auth/auth-code-error. Build that page even if it is just a sentence and a link back to sign-in. A blank screen on a failed login is the worst possible first impression.
Step 4: Fix the redirect URLs (this is what breaks)
Almost every "OAuth isn't working" report comes down to a redirect URL that does not match across the three systems. Here is the map of which URL goes where.
| Where | What URL it needs | Example |
| --- | --- | --- |
| Google / GitHub console (redirect URI) | Your Supabase callback | https://<id>.supabase.co/auth/v1/callback |
| Google console (JS origins) | Your app's base URL | https://yourapp.com, http://localhost:3000 |
| Supabase → URL Configuration | Your app's allowed redirects | https://yourapp.com/**, http://localhost:3000/** |
| signInWithOAuth redirectTo | Your callback route | https://yourapp.com/auth/callback |
The one founders miss most often is the Supabase redirect allow list. In the Supabase dashboard under Authentication → URL Configuration, you have to explicitly add the URLs your app is allowed to redirect to after login. If redirectTo points somewhere not on that list, Supabase silently falls back to your site URL and the login looks broken for no visible reason. The Supabase redirect URLs documentation covers the wildcard patterns — https://yourapp.com/** matches any path on your domain.
Two more that bite:
- Localhost during development. Add
http://localhost:3000(or whatever port you use) to both Google's JS origins and Supabase's allow list. Then remove localhost before you launch, so a leaked link cannot redirect through your dev environment. - Trailing slashes and http vs https.
http://yourapp.comandhttps://yourapp.com/are three different strings to an OAuth provider. Match them exactly.
If you want a refresh token from Google — needed only if you plan to call Google's own APIs on the user's behalf later — pass queryParams: { access_type: 'offline', prompt: 'consent' } in the signInWithOAuth options. Google does not send a refresh token by default. Most apps never need this; skip it until you do.
Letting AI wire it up for you
If you are building with an AI coding assistant, OAuth setup is a good candidate to delegate — the code is well-documented and the structure is predictable. The trick is giving the assistant the constraints, not just the goal.
A prompt that works looks like this: "Add Google and GitHub OAuth to my Next.js App Router app using Supabase Auth and the PKCE flow. Create the callback route at app/auth/callback/route.ts using exchangeCodeForSession. Use my existing createClient server helper. Don't hardcode the redirect URL — derive it from the request origin. Add an /auth/auth-code-error page." That gives the model the file paths and the flow, which is where it would otherwise guess wrong.
What AI cannot do is click around the Google Cloud and Supabase dashboards for you. The provider setup and the redirect-URL configuration are manual, and they are exactly the steps where things break — so do those by hand and verify them yourself. The patterns for writing prompts that produce working code instead of plausible-looking code are in prompt engineering for non-developers, and the broader workflow is in Claude Code for non-developers.
This is the same division of labor that runs through the whole Next.js + Supabase + Stripe + Resend stack: let the assistant write the predictable code, and keep the dashboard configuration and the verification in your own hands.
Reading the logged-in user after login
Wiring up the login is only useful if the rest of your app can tell who is signed in. Once exchangeCodeForSession has run, the session lives in cookies, and any server component can read the current user without another round trip to the provider.
In a server component or route handler, you create the server Supabase client and ask for the user:
import { createClient } from '@/utils/supabase/server'
export default async function DashboardPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
// not signed in — send them to the sign-in page
}
return <p>Signed in as {user?.email}</p>
}
Two habits matter here. First, use getUser() rather than reading the session object directly when you need to trust the result — getUser() validates the token with Supabase, so it cannot be spoofed by a tampered cookie. Reading the raw session is fine for rendering a name, not for gating access to paid features.
Second, the user object is the same shape no matter how someone logged in. Whether they used Google, GitHub, or email, you get one user.id and one user.email. That consistency is the real payoff of routing everything through Supabase Auth — your product code never has to branch on which provider someone chose. When you later add gated routes, billing, or per-user data, they all key off that one stable user.id.
If you need provider-specific data — a GitHub username, a Google avatar — it arrives in user.user_metadata. Pull what you need into your own profile table on first login rather than reaching into that object everywhere; it keeps your product code independent of provider quirks. This is the same pattern the Stripe payments tutorial uses when it ties a Stripe customer to a Supabase user — one stable ID, everything hangs off it.
Frequently asked questions
Do I need a callback route for OAuth with Supabase?
For a server-side Next.js app using the PKCE flow, yes. The provider returns a one-time code, and app/auth/callback/route.ts is where you call exchangeCodeForSession to turn that code into a session stored in cookies. The implicit flow skips the route but is not recommended for server-rendered apps.
Why does my Google login redirect to the wrong page?
Almost always a redirect-URL mismatch. Check that your redirectTo value is on the Supabase allow list under Authentication → URL Configuration, and that Google's authorized redirect URI points at your Supabase callback (https://<id>.supabase.co/auth/v1/callback), not your app. Three systems have to agree.
Can I use both OAuth and email/password login?
Yes. Supabase Auth supports multiple methods on the same project and the same user. A common setup is Google plus GitHub plus a magic-link email fallback, so visitors without either provider account can still sign up. They all resolve to one user object.
How do I get a refresh token from Google?
Pass queryParams: { access_type: 'offline', prompt: 'consent' } in the signInWithOAuth options. Google omits the refresh token by default. You only need this if you intend to call Google's own APIs on the user's behalf later — for plain login, the Supabase session is all you need.
Is OAuth secure enough for a production SaaS?
OAuth via Supabase with the PKCE flow is a standard, production-grade approach used by thousands of apps. The security work that matters is on your side: keep the next.startsWith('/') redirect guard, store secrets only in environment variables, and remove localhost from your allow lists before launch.
Wrapping up
OAuth with Supabase is three parts — provider config, the signInWithOAuth call, and the callback route — and the only genuinely fiddly part is making the redirect URLs agree across Supabase, Google, and GitHub. Do the dashboard steps carefully, keep the code close to the Supabase reference, and verify the round trip on localhost before you touch production.
If you would rather start from a codebase where the auth callback route, the Supabase clients, and the session handling are already wired and tested, Coding Capybaras is the free boilerplate I built for exactly this — and the integration marketplace has copy-paste prompts for the rest of the stack.