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.
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.
// 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):
https://yourdomain.comThis 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):
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:
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:
'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:
| Environment | NEXT_PUBLIC_SITE_URL |
|---|---|
| Production | https://yourdomain.com |
| Preview | (leave unset — falls back to NEXT_PUBLIC_VERCEL_URL) |
| Development | http://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 observe | Where to investigate |
|---|---|
redirect_to already contains production | The helper's environment variables or a hardcoded URL |
redirect_to contains the preview, but the destination falls back | Supabase Redirect URLs and the exact host/path match |
The provider itself rejects redirect_uri | The provider's registered Supabase callback URL |
| The browser reaches the preview callback but no session exists | The callback's code exchange and cookie handling |
| OAuth works, but an email link goes to production | That 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 }}:
<!-- 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:
<!-- 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_URLset 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
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
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.
Continue Reading
Fix: Supabase redirectTo Ignored on Magic Link / Email Auth
Magic links and password-reset emails that always land on the production Site URL instead of the redirectTo you passed. The allow-list mechanism behind it, and the wildcard pattern for local dev and preview branches.
Supabase Auth Session Disappears on Refresh? Fix (2026)
Supabase auth sessions mysteriously disappearing after page refresh? Learn the exact cause and fix it in 5 minutes with this tested solution.
Fix Supabase: Invalid Refresh Token Already Used
Trace which client retains an old Supabase token, preserve refreshed response cookies, and let the affected browser sign in again without a global logout.


