# How to add Google sign-in to a Next.js + Supabase app

> Add Sign in with Google to a Next.js app with Supabase Auth: the Google OAuth client, both redirect URLs, signInWithOAuth and a PKCE callback route.

Source: https://www.shipanysaas.com/blog/nextjs-supabase-google-sign-in
Published: 2026-10-18

Supabase Google auth gives your Next.js app a "Sign in with Google" button without running an OAuth server yourself. Google checks who the user is, Supabase turns that into a session for your project, and your app receives the session in a cookie through one callback route.

How do you add it? Create a Web application OAuth client in Google Cloud and give it your Supabase project's callback URL. Paste its client ID and secret into the Google provider in Supabase, and allow your app's `/auth/callback` URL. Then add a button that calls `signInWithOAuth({ provider: 'google' })`, and a route handler that swaps the returned code for a session. Almost every failure comes from one of two redirect URLs, so this guide keeps them apart.

## How Supabase Google auth works

A Google OAuth login makes two hops after the user picks an account:

1. **Google to Supabase.** Google sends the user to your Supabase project's callback, `https://<project-ref>.supabase.co/auth/v1/callback`. This is the URL Google must know about.
2. **Supabase to your app.** Supabase sends the user to the `redirectTo` you passed, such as `https://example.com/auth/callback?code=...`. This is the URL Supabase must allow.

With `@supabase/ssr`, the client uses the PKCE flow, so the second hop carries a one-time `code`, not tokens. Your callback route trades it for a session with `exchangeCodeForSession()`. Supabase's [PKCE docs](https://supabase.com/docs/guides/auth/sessions/pkce-flow) say the code is valid for five minutes and works once.

## Step 1: create the Google OAuth client

Everything on Google's side happens in the Google Auth Platform section of Google Cloud. Supabase's [Google sign-in guide](https://supabase.com/docs/guides/auth/social-login/auth-google) walks through the screens. In short:

- **Audience**: choose who may sign in.
- **Data Access**: Supabase needs `openid`, which you add by hand, plus the email and profile scopes, which are there by default. Extra sensitive scopes can mean a long review.
- **Branding**: add your app's name and logo. Brand verification can take a few business days.
- **Clients**: create an OAuth client ID with the type **Web application**.

The client has two lists, and they hold different things:

| Field | What goes in it |
| --- | --- |
| Authorized JavaScript origins | Your app's origin, such as `https://example.com`, plus `http://localhost:3000` while you develop |
| Authorized redirect URIs | Your Supabase project's callback URL, copied from the Google provider page in the Supabase dashboard |

For the local Supabase stack, the redirect URI is `http://127.0.0.1:54321/auth/v1/callback`. Save the client ID and client secret when Google shows them.

## Step 2: turn on Supabase Google auth

In the Supabase dashboard, open the Google provider under Authentication, turn it on, and paste the client ID and secret.

Then open **URL Configuration**. Set the Site URL to your production domain, and add your callback, `https://example.com/auth/callback`, to the redirect URLs. Add `http://localhost:3000/auth/callback` for development.

The local stack reads the provider from `supabase/config.toml`, with the secret kept in an environment variable:

```toml
[auth.external.google]
enabled = true
client_id = "<client-id>"
secret = "env(SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_SECRET)"
skip_nonce_check = false
```

## Step 3: add the Sign in with Google button

In the browser, `signInWithOAuth()` sends the user to Google itself. The button is a small Client Component built on the browser client from Supabase's [Next.js server-side auth guide](https://supabase.com/docs/guides/auth/server-side/creating-a-client?queryGroups=framework&framework=nextjs):

```tsx
'use client';

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

export function GoogleSignInButton() {
  async function signIn() {
    const supabase = createClient();

    const { error } = await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: { redirectTo: `${window.location.origin}/auth/callback` },
    });

    if (error) {
      console.error(error.message);
    }
  }

  return (
    <button type="button" onClick={signIn}>
      Sign in with Google
    </button>
  );
}
```

Before it leaves, the client stores a PKCE code verifier in a cookie. The callback needs it, which is why the sign-in has to finish in the same browser that started it.

You can start the flow on the server instead, from a Server Action, with the server client and `redirect(data.url)`. Supabase's guide notes that the verifier cookie must reach the browser with that redirect. In Next.js, the server client writes it through `cookies()`, so it does.

## Step 4: handle the OAuth callback

The callback is a route handler at `app/auth/callback/route.ts`. It uses the server client, which can set cookies in a route handler:

```typescript
// app/auth/callback/route.ts
import { type NextRequest, NextResponse } from 'next/server';

import { createClient } from '@/lib/supabase/server';

export async function GET(request: NextRequest) {
  const { searchParams, origin } = request.nextUrl;
  const code = searchParams.get('code');

  if (code) {
    const supabase = await createClient();
    const { error } = await supabase.auth.exchangeCodeForSession(code);

    if (!error) {
      return NextResponse.redirect(new URL('/dashboard', origin));
    }
  }

  return NextResponse.redirect(new URL('/auth/error', origin));
}
```

Supabase's example also reads a `next` parameter, so users return to the page they started on. If you add one, don't stop at checking for a leading `/`, because `//evil.example` passes that test. Resolve the value against your origin and follow it only when the origin still matches.

From then on, Google users are ordinary Supabase users. `proxy.ts` refreshes their sessions, `getClaims()` protects your pages, and row-level security applies to them like everyone else.

## Common Google OAuth login errors

| What you see | Usual cause |
| --- | --- |
| Google shows `redirect_uri_mismatch` | The Supabase callback URL in Google Cloud doesn't match the one on the Supabase provider page exactly |
| After Google, you land on your Site URL, not your callback | Your `redirectTo` isn't on Supabase's redirect URL list, so Supabase falls back to the Site URL |
| The callback fails with a code verifier error | The flow finished in a different browser, or the verifier cookie never reached the browser |
| Works locally, fails in production | The production callback isn't on Supabase's redirect URL list, or the Site URL still points to localhost |
| The consent screen shows `<project-ref>.supabase.co` | Expected without a custom domain for your Supabase project |

On that last one, Supabase recommends a custom domain such as `auth.example.com`, so people see a name they recognize before they approve.

## Going further

**Google's own tokens.** The session your app stores is Supabase's. If you also call Google APIs for the user, read `provider_token` from the session when the user signs in, because Supabase doesn't keep it for you. Google sends no refresh token unless you ask, with `queryParams: { access_type: 'offline', prompt: 'consent' }` in the `signInWithOAuth()` options.

**One Tap.** Google's pre-built buttons and One Tap work too. They hand you an ID token, which `signInWithIdToken()` turns into a session, and Supabase's guide has a Next.js example.

**Accounts with the same email.** Supabase's [identity linking docs](https://supabase.com/docs/guides/auth/auth-identity-linking) say identities with the same email address are linked to one user automatically. Someone who signed up with a password and later clicks Google ends up in the same account. To guard against account takeover, Supabase removes that user's other unconfirmed identities when it links one.

## How the kit does it

In shipanysaas, Google is the default OAuth provider. With `NEXT_PUBLIC_AUTH_OAUTH_PROVIDERS` unset, the sign-in page shows a Google button. A comma-separated list such as `google,github` adds more, and `none` hides them all. You still create the Google client and turn on the provider in Supabase yourself. The local `apps/web/supabase/config.toml` turns on no OAuth provider, so the button fails locally until you add the `[auth.external.google]` block or set the variable to `none`.

The button in `packages/features/auth/src/components/oauth-providers.tsx` calls `signInWithOAuth()` with a `redirectTo` of `/auth/callback`, carrying a `next` path. The callback route, `apps/web/app/[locale]/auth/callback/route.ts`, exchanges the code and follows `next` only after `isSafeRedirectPath` accepts it. With `NEXT_PUBLIC_AUTH_IDENTITY_LINKING=true` and manual linking turned on in Supabase, users can also link Google to an existing account from their settings. The phone app in the All-in plan signs in with Google through the same Supabase project, using the `shipanysaas://` scheme as its redirect.

The [live demo](https://shipanysaas-demo.vercel.app/auth/sign-in) runs with OAuth buttons off, so it signs in with a password. The provider setup is in [OAuth providers](https://www.shipanysaas.com/docs/authentication/oauth-providers), and the plans are on the [pricing](https://www.shipanysaas.com/pricing) page. For a passwordless option next to Google, see the [magic link guide](https://www.shipanysaas.com/blog/nextjs-supabase-magic-link).

## Questions people ask

### Which redirect URL goes in Google Cloud?

Only your Supabase project's callback, `https://<project-ref>.supabase.co/auth/v1/callback`, or your Supabase custom domain's equivalent. Your app's own `/auth/callback` goes in Supabase's redirect URL list, not in Google.

### Do I need a separate client for each environment?

You can use one Web client with every origin listed, or separate clients per environment. Either way, each Supabase project needs its own callback URL in the client's redirect URIs.

### Does Supabase store the user's Google access token?

No. You get `provider_token` in the session at sign-in. If you need Google APIs later, store it yourself, along with a refresh token requested with `access_type: 'offline'`.

### Can the same user also sign in with email?

Yes, once the email is confirmed. Supabase links identities with the same email to one user automatically, so a confirmed password account and Google open the same account. When it links a new identity, it removes the user's unconfirmed ones, so a password sign-up that was never confirmed doesn't survive a Google sign-in.
