Supabase

Supabase redirectTo Failing on Vercel Previews: SITE_URL Fix

The redirect silently lands on your production domain instead of the preview URL, with no error from Supabase. Here is the exact Supabase dashboard config and the Vercel env var pattern.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
6 min read

This is the auth bug that only appears when a teammate opens a Vercel preview link and tries to log in. There is no error from Supabase: after sign-in, the browser is sent to the production Site URL (for example https://myapp.com/auth/callback) instead of the preview deployment (https://myapp-git-feature-team.vercel.app/auth/callback). If a redirect_uri_mismatch error does appear, it comes from the OAuth provider (Google, GitHub and so on), not from Supabase; see the provider section further down.

How Supabase redirect URLs work#

Supabase maintains an allowlist of permitted redirect destinations. Per Supabase's own Redirect URLs docs, "the Site URL in URL Configuration defines the default redirect URL when no redirectTo is specified in the code," and every redirectTo value passed in auth calls is checked against that allowlist. A URL that is not allowed does not produce an error. The redirect check in the Supabase Auth server falls back to the request's Referer if that one is valid, and otherwise to the Site URL. A preview URL missing from the list therefore sends the user to production, silently.

js
// If the current preview URL is not allowed, Supabase silently falls back to the Site URL
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: `${window.location.origin}/auth/callback`,
  },
})

window.location.origin on a preview deployment returns something like https://myapp-git-feature-branch-myteam.vercel.app — a different host from the Site URL that matches no pattern in the list, so the user ends up on the Site URL instead.

The dashboard config#

Location: Supabase Dashboard → Authentication → URL Configuration

You need three things configured:

Site URL (single value):

plaintext
https://yourdomain.com

This is the fallback URL for emails (confirmation, password reset) when no redirectTo is specified. In production this must be your real domain, not http://localhost:3000.

Redirect URLs (allowlist):

plaintext
http://localhost:3000/**
https://yourdomain.com/**
https://*-your-team-slug.vercel.app/**

The ** wildcard matches any sequence of characters. The pattern https://*-your-team-slug.vercel.app/** covers every preview deployment URL that Vercel generates for your team — it matches the exact Vercel wildcard example in Supabase's redirect URL docs (https://*-<team-or-account-slug>.vercel.app/**).

How to find your team slug: in Vercel, go to Settings → General → URL Slug. Your preview URLs follow the format <project>-<branch>-<slug>.vercel.app.

The environment variable pattern for Vercel#

The redirectTo in your code must be dynamic — it should use the actual URL of the current deployment, not a hardcoded production URL.

The recommended pattern from the Supabase docs:

js
export function getSiteURL() {
  let url =
    process?.env?.NEXT_PUBLIC_SITE_URL ??
    process?.env?.NEXT_PUBLIC_VERCEL_URL ??
    'http://localhost:3000/'
 
  // Vercel provides NEXT_PUBLIC_VERCEL_URL without https://
  url = url.startsWith('http') ? url : `https://${url}`
  // Ensure trailing slash
  url = url.endsWith('/') ? url : `${url}/`
 
  return url
}

Then use it when calling auth:

js
'use server'
 
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
import { getSiteURL } from '@/lib/utils'
 
export async function signInWithGitHub() {
  const supabase = await createClient()
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: 'github',
    options: {
      redirectTo: `${getSiteURL()}auth/callback`,
    },
  })
 
  if (error) throw new Error(error.message)
  if (!data.url) throw new Error('Supabase did not return an OAuth URL')
  redirect(data.url)
}

In Vercel environment variables:

EnvironmentNEXT_PUBLIC_SITE_URL
Productionhttps://yourdomain.com
Preview(leave unset — falls back to NEXT_PUBLIC_VERCEL_URL)
Developmenthttp://localhost:3000

Vercel exposes the deployment host through system environment variables. Check that system variables are available to the build and that a production NEXT_PUBLIC_SITE_URL has not also been assigned to the Preview environment. The helper chooses NEXT_PUBLIC_SITE_URL first, so an inherited production value wins even when the preview host is available. After changing an environment variable, redeploy the preview before testing again.

Verify the redirect before changing email templates#

Use the browser's Network panel during a fresh OAuth sign-in. Inspect the request to your Supabase project's /auth/v1/authorize endpoint and decode its redirect_to query parameter. It should be the current preview origin plus /auth/callback, not your production origin.

What you observeWhere to investigate
redirect_to already contains productionThe helper's environment variables or a hardcoded URL
redirect_to contains the preview, but the destination falls backSupabase Redirect URLs and the exact host/path match
The provider itself rejects redirect_uriThe provider's registered Supabase callback URL
The browser reaches the preview callback but no session existsThe callback's code exchange and cookie handling
OAuth works, but an email link goes to productionThat email flow's redirect option and custom template

This separates URL selection from session creation. A valid allowlist entry does not implement the callback route or exchange an authorisation code. Compare the complete flow with the Supabase Next.js server-side auth guide. Do not share full callback URLs containing codes or email token hashes when collecting diagnostics.

Email templates: when SITE_URL overrides redirectTo#

The default email templates build their link from {{ .ConfirmationURL }}, which already carries redirect_to. With the default templates, the allowlist above is all that is needed.

The problem only appears with a custom template whose link is built on {{ .SiteURL }}:

html
<!-- Custom template: always points at the Site URL, ignores redirectTo -->
<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email">Confirm your email</a>

If the Site URL is http://localhost:3000 (the Supabase default before it is changed), production password reset emails send users to localhost; on previews, every email link goes to production.

For this custom confirmation flow, build the link on the destination origin passed by the application, and preserve the token parameters. The following example assumes the email auth call passes a bare origin such as https://myapp-git-feature-team.vercel.app as its email redirect destination:

html
<!-- Custom template: follows the redirectTo passed in code -->
<a href="{{ .RedirectTo }}/auth/confirm?token_hash={{ .TokenHash }}&type=email">Confirm your email</a>

Do not append /auth/confirm if the supplied redirect destination already includes that path. Do not copy type=email into every template: recovery, invitation and email-change flows need their own matching verification handling. Keep the default ConfirmationURL unless you deliberately implement a custom token-hash flow. The email-template variables and examples document these separate flows.

The /auth/confirm route must exist on the preview deployment and verify the token hash with the correct OTP type before establishing a session. That is a different operation from exchanging an OAuth callback's code. A missing route gives a 404 even when the allowlist is correct.

Location in Supabase Dashboard: Authentication → Email Templates

If no message arrives at all, diagnose delivery separately with Supabase email confirmation troubleshooting. Changing a redirect URL cannot fix a message that was never delivered.

Which URLs need an allowlist entry#

URLs on the same scheme, host and port as the Site URL are always allowed; other hosts need an allowlist entry, and /** covers every path. That is why production keeps working with a sparse list: https://yourdomain.com/auth/callback shares the Site URL's host. A Vercel preview is a different host, so it needs its own entry, https://*-your-team-slug.vercel.app/**, or Supabase falls back to the Site URL without saying so.

When the fix is not just a URL allowlist#

If previews work but production email redirects still go to the wrong URL, the problem is usually a custom email template built on {{ .SiteURL }}, not the allowlist. Both failures are silent: a redirectTo outside the allowlist falls back to the Site URL, and a template built on {{ .SiteURL }} ignores redirectTo altogether. Neither returns an error; the user just lands somewhere unexpected. When only email links are wrong, check the templates first; when OAuth sign-ins are wrong too, check the allowlist first.

window.location.origin only belongs in client-only code: it is a browser API, so in a Server Action or route handler window is undefined. On the server, use the NEXT_PUBLIC_SITE_URL or NEXT_PUBLIC_VERCEL_URL env vars (through the getSiteURL() helper above) instead.

Checklist#

  • Supabase Dashboard → Authentication → URL Configuration: Site URL set to production domain (not localhost).
  • Redirect URLs allowlist includes http://localhost:3000/**.
  • Redirect URLs allowlist includes https://yourdomain.com/**.
  • Redirect URLs allowlist includes https://*-your-team.vercel.app/**.
  • NEXT_PUBLIC_SITE_URL set to production domain in Vercel production environment.
  • If you use custom templates, make them carry {{ .RedirectTo }} alongside {{ .TokenHash }}.
  • Test: open a fresh Vercel preview URL, click "Forgot password", verify email link goes to the preview URL (not production).

The OAuth provider's own redirect URI is not your app's URL#

Everything above fixes Supabase's allowlist. There is a second, separate allowlist that lives outside Supabase entirely: the OAuth provider itself — a GitHub OAuth App, a Google Cloud Console OAuth client, or similar. That second allowlist is easy to misconfigure in exactly the wrong way when chasing a preview-deployment bug.

The confusion: when signInWithOAuth runs, the browser goes to the provider (GitHub, Google, etc.), the provider authenticates the user, and then the provider redirects back — but not to your app. It redirects to Supabase's own fixed callback endpoint, https://<project-ref>.supabase.co/auth/v1/callback. Only after Supabase receives that callback does Supabase read its own redirect allowlist (the one covered above) and send the browser onward to your app's redirectTo — the preview URL.

That means the redirect URI registered in the GitHub OAuth App or Google Cloud Console should almost always be the single, fixed Supabase callback URL — never a Vercel preview URL, and never a wildcard. Adding https://*-your-team.vercel.app/** to the provider's settings does nothing useful there, because most OAuth providers either reject wildcard redirect URIs outright or ignore them, and the provider was never going to redirect to your app directly in the first place. If sign-in fails with an error page rendered by the provider itself (GitHub's or Google's own error page, not a redirect to your app) rather than your app showing a broken callback, the mismatch is on the provider side, not in Supabase's Redirect URLs list — a useful way to tell which of the two allowlists is actually wrong.

For the exact dashboard steps in Google's console specifically — including where the fixed Supabase callback URL comes from and how to verify it — see Supabase + Google OAuth on Next.js 15: Working Guide (2026). The same one-callback-URL rule applies to GitHub, GitLab, and every other OAuth provider Supabase supports: one fixed entry on the provider side, the flexible wildcard list on the Supabase side.

Further reading: Fix: Supabase redirectTo Ignored on Magic Link / Email Auth.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.