I was setting up Google login while building SizoCare recently. The Next.js frontend was running locally, Supabase was on the cloud, and Google was handling authentication.
This should have been a 10-minute job.
Then I had three dashboards open, several callback URLs that looked almost identical, and Google throwing redirect_uri_mismatch.
If you're vibe coding your first proper app, OAuth is one of those things your coding agent can implement for you while still leaving you with absolutely no idea why it works.
So here's what I wish I'd understood before setting it up.
You probably don't need local Supabase
My first confusion was whether I needed to run Supabase locally because my Next.js app was running on localhost.
I didn't. I already had a Supabase Cloud project, so I kept using it.
My setup was simply:
Next.js on localhost
↓
Supabase Cloud
↓
GoogleYour frontend being local doesn't require your database and authentication server to be local too.
Supabase does have a local development stack. It is useful when you're working heavily with migrations, testing database changes, running integration tests, or deliberately keeping development isolated from a hosted environment. But you don't need to introduce it just to make Google login work.
For an MVP, I'd rather have one backend to think about than accidentally spend an hour debugging the local database when the app is connected to the cloud one.
The confusing part is the callback
For these examples, let's say SizoCare runs at http://localhost:3000 and its hosted Supabase project is https://YOUR_PROJECT_REF.supabase.co. These are placeholders; use your own project and the port your app actually runs on.
There are three parties involved when someone clicks Continue with Google:
SizoCare → Supabase → Google
And on the way back:
Google → Supabase → SizoCareThat second line explains most of the configuration. In this Supabase OAuth redirect flow, Google doesn't send the user directly back to my Next.js application. It sends them to Supabase first.
So inside Google Cloud, the authorized redirect URI is:
https://YOUR_PROJECT_REF.supabase.co/auth/v1/callbackThen Supabase sends the user back to my application:
http://localhost:3000/auth/callbackThese are two different callbacks for two different hops. I initially treated them as interchangeable. They aren't.
Setting up Google
Go to Google Cloud Console and create or select a project. Open Google Auth Platform and configure the app's branding and audience. For a personal development project, an external audience is usually the relevant choice; if the app is in Testing, add your own Google account as a test user where required.
Then create an OAuth Client ID and choose Web application.
For local development, add this as an authorized JavaScript origin:
http://localhost:3000Now add your hosted Supabase callback as the authorized redirect URI. Copy the exact URL shown in Supabase's Google provider settings:
https://YOUR_PROJECT_REF.supabase.co/auth/v1/callbackGoogle will give you a Client ID and Client Secret. The Client ID isn't a secret. The Client Secret is.
Don't paste the secret into ChatGPT, Claude, Cursor, or whichever agent is writing the app with you. Your coding agent does not need the real value to understand a variable called GOOGLE_CLIENT_SECRET. It needs to know the variable exists.
Then configure Supabase
In Supabase, go to Authentication → Sign In / Providers → Google. Enable Google and enter the Client ID and Client Secret you got from Google.
Then go to Authentication → URL Configuration. For a project still being developed locally, I use:
Site URL
http://localhost:3000
Allowed redirect URL
http://localhost:3000/auth/callbackWhen the application goes into production, set the Site URL to its production address and add its callback to the redirect allow list:
https://myapp.com/auth/callbackA shared Supabase project can allow both development and production callbacks while you still need them. I keep the entries explicit. If you use separate projects or Google OAuth clients for development and production, configure each against its own URLs.
Your local environment is much simpler
My Next.js application needs the public Supabase connection details. A current setup can use a publishable key:
NEXT_PUBLIC_SUPABASE_URL=https://YOUR_PROJECT_REF.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_PUBLIC_KEYIf your app already uses the legacy anon key, its configuration may instead be named NEXT_PUBLIC_SUPABASE_ANON_KEY. Use the variable name your Supabase client code actually reads. The anon and publishable keys are designed for browser use.
Your app may also define public settings like these. They are application conventions, not requirements imposed by Google or Supabase:
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_APP_NAME=SizoCareGet the Supabase URL and public key from the project's Connect dialog or API settings. For this hosted provider flow, the Google Client Secret belongs in Supabase's provider configuration; my Next.js frontend doesn't need it.
One thing worth checking carefully: service_role and Supabase secret keys should never sit inside a NEXT_PUBLIC_* variable. NEXT_PUBLIC_ means the value can be exposed to the browser. A service-role key can bypass Row Level Security.
After changing .env.local, restart the development server. SizoCare is a pnpm workspace, so from the repository root I use:
pnpm --filter @sizocare/web devFor a standalone app with a dev script, pnpm dev may be all you need.
The app callback still has a job to do
Adding a URL to a dashboard doesn't create that route in your application.
In SizoCare, the Google button calls signInWithOAuth with provider: 'google' and a redirectTo built from the current browser origin:
// Using your configured browser Supabase client
await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});With Next.js server-side, cookie-based authentication, Supabase returns a code to that app callback. The route must use the configured server Supabase client to exchangeCodeForSession(code), save the session through its cookie handling, and redirect the user into the app. It also needs an error path if that exchange fails.
That is the implementation I want my coding agent to handle. I still want to understand why the route exists.
If Google gives you redirect_uri_mismatch
This error looks more complicated than it usually is. Google will show the redirect URI the sign-in request tried to use. Copy it and compare it with the authorized redirect URI in the matching Google OAuth client. Character for character.
For the hosted Supabase flow described here, Google's redirect URI should be the Supabase callback. Your localhost app callback belongs in Supabase's allow list and the application's redirectTo setting.
Keep the app addresses consistent too. These aren't necessarily interchangeable:
http://localhost:3000
http://127.0.0.1:3000
https://localhost:3000
/callback
/auth/callback
/auth/v1/callbackDon't start changing your authentication code until you've checked which hop is failing. I nearly went down that route when the problem was configuration.
Getting logged in isn't the end of the backend work
Once Google login works, you have proved that the application knows who the user is. You haven't proved that the user can only access their own data.
If I'm logged in as User A, changing an ID in a request shouldn't suddenly let me read User B's records.
With Supabase, this is where Row Level Security matters. Your frontend might only display records belonging to the current user. That's nice. The database should enforce the access rules too.
For a caregiver product like SizoCare, access can involve the person receiving care and the people they've allowed to help. The policies need to match those relationships. A successful login doesn't verify them.
I treat these as separate tasks:
- Can the user sign in?
- Can the user access only what they're supposed to?
Both need to work. Test with separate accounts and requests for records they shouldn't be able to access.
One rule I've started following while vibe coding
AI coding tools ask for environment configuration constantly. I give them the names and placeholders:
SUPABASE_URL
SUPABASE_ANON_KEY
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRETThose names describe the configuration. They aren't a list of secrets my frontend needs, and I don't paste GOOGLE_CLIENT_SECRET followed by its real value.
Same goes for database passwords, service-role keys, private API keys, payment credentials, and access tokens. Put real values directly into the service that needs them: an ignored .env.local, Supabase's provider settings or function secrets, Vercel environment settings, or CI/CD secrets.
Keep them out of tracked files, screenshots, and logs too. The coding agent usually needs the shape of your configuration, not your credentials.
That distinction becomes fairly important once your vibe-coded weekend project starts containing real users and real data.
The bit worth remembering
If you're using Supabase's Google OAuth redirect flow, remember the return trip:
Google
↓
Supabase callback
↓
Your app callback
Google Cloud gets:
https://YOUR_PROJECT_REF.supabase.co/auth/v1/callback
Supabase's redirect allow list gets:
http://localhost:3000/auth/callbackAnd if you're already using Supabase Cloud, you don't need to spin up local Supabase just because you're developing the frontend locally.
That was basically the entire problem. It just took three dashboards to explain it.