How to add Google sign-in to Next.js with Supabase

Connect Google OAuth to Supabase Auth in Next.js, exchange the callback code, and verify the server session.

By Antoine RossPublished Facts verified

Google sign-in sends the user to Google, returns them through Supabase Auth, and exchanges a short-lived code in your app for a cookie-backed session. Your server must still verify that session and authorize each protected operation.

Before you start

Complete how to add authentication to Next.js first. This guide assumes its lib/supabase/client.ts and lib/supabase/server.ts helpers, a protected /dashboard, and a working cookie refresh boundary. Install both @supabase/supabase-js and @supabase/ssr as described there. Follow Supabase's maintained SSR client and refresh setup: on Next.js 16 the entrypoint is ; on Next.js 15 and earlier it is . Refreshing cookies does not replace authorization.

You need a Supabase project, a Google Cloud project, and a local app origin such as http://localhost:3000. Use the same origin throughout the local flow. The browser-visible Supabase URL and publishable key belong in your app configuration; the Google client secret belongs in the Supabase provider configuration, never browser code. Set the server-only SITE_URL to the trusted app origin for this environment, such as http://localhost:3000.

1. Configure Google and Supabase

In Google Cloud, configure the OAuth consent screen and create a web application OAuth client. Add your Supabase project's callback URL as an authorized redirect URI in Google. Supabase displays that URL on its Google provider configuration page; it is distinct from your application's callback URL.

Enable Google in Supabase Auth and enter the Google client ID and secret. In Supabase URL configuration, allow your app callback, for example http://localhost:3000/auth/callback. For deployment, add the actual public callback and set the intended site URL. Follow the current Supabase Google provider instructions for Google consent and production requirements.

2. Add a sign-in button

This Client Component uses the browser helper from the prerequisite. It returns the user to a fixed callback on the current app origin.

app/login/google-button.tsx
'use client';

import { useState } from 'react';
import { createClient } from '@/lib/supabase/client';

export function GoogleButton() {
  const [error, setError] = useState('');

  async function signIn() {
    setError('');
    const supabase = createClient();
    const { error } = await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: { redirectTo: `${window.location.origin}/auth/callback` },
    });
    if (error) setError('Could not start Google sign-in. Please try again.');
  }

  return (
    <div>
      <button type="button" onClick={signIn}>Continue with Google</button>
      {error && <p role="alert">{error}</p>}
    </div>
  );
}

Render it on your login page:

app/login/page.tsx
import { GoogleButton } from './google-button';

export default async function LoginPage({
  searchParams,
}: {
  searchParams: Promise<{ error?: string }>;
}) {
  const { error } = await searchParams;
  return (
    <main>
      <h1>Sign in</h1>
      {error === 'oauth' && <p role="alert">Sign-in could not be completed. Please try again.</p>}
      <GoogleButton />
    </main>
  );
}

This example uses the asynchronous searchParams convention of current Next.js App Router. If you already have a login page, add the import, button and error message to that page instead of replacing its existing form.

3. Exchange the callback code

Create a Route Handler that uses the cookie-aware server client. It exchanges the authorization code and redirects to a fixed local destination; it never accepts an arbitrary redirect URL from a query parameter.

app/auth/callback/route.ts
import { NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';

export async function GET(request: Request) {
  const siteUrl = process.env.SITE_URL;
  if (!siteUrl) return new Response('Site URL is not configured', { status: 500 });
  const code = new URL(request.url).searchParams.get('code');

  if (code) {
    const supabase = await createClient();
    const { error } = await supabase.auth.exchangeCodeForSession(code);
    if (!error) return NextResponse.redirect(new URL('/dashboard', siteUrl));
  }

  return NextResponse.redirect(new URL('/login?error=oauth', siteUrl));
}

The login page shows a generic retry message for error=oauth; do not display the code or provider secrets. Set SITE_URL separately for local and deployed environments, and use only an origin you control. If you later support a return path, validate it against a list of allowed local paths rather than redirecting to a caller-supplied URL. The Supabase callback guidance explains the PKCE exchange and cookie requirement.

4. Verify the whole session

Start your app and sign in with a permitted Google test account. Confirm the callback reaches /dashboard, a server-rendered protected page recognizes the user, and a reload keeps the session. Sign out and confirm a fresh request to /dashboard is rejected. Test a denied Google consent and an invalid or missing callback code: both should return to login without granting access. Test again after the session needs refresh and confirm that the refresh boundary returns updated cookies.

If Google reports a redirect mismatch, compare the exact Supabase callback configured in Google. If Supabase rejects the return URL, compare the app callback with its allow list. A local localhost and 127.0.0.1 mismatch is enough to break a flow.

CoreMVP's authentication Docs describe the shipped Supabase OAuth, callback and server session path. That saves the work of connecting the flow yourself; you still configure your own Google and Supabase projects and verify the deployed callback.

FAQ

What happens if a user already has a password account?

Supabase can automatically link a new OAuth identity to an existing account with the same verified email, subject to its identity-linking rules. Unconfirmed identities and SAML SSO have special handling. Test the actual existing-account case, and consult Supabase's identity-linking documentation before changing the flow. Do not implement your own email-based merge.

Why can my test account sign in but another Google account cannot?

Check the Google OAuth audience and publishing status. An external application in Testing can limit access to accounts listed as test users; internal applications are restricted to the organization. Google documents the audience and publishing settings and app states. Check the requested scopes and current settings before deciding whether you need to add a test user or complete production verification.