This is the auth bug that only appears when a teammate opens a Vercel preview link and tries to log in. The error from Supabase: redirect_uri_mismatch or the redirect silently goes to production instead of the preview.
I hit this while QA-ing an OAuth flow. Everything worked fine on production. Preview deployments got a blank screen after the OAuth callback — because Supabase was sending the user to https://myapp.com/auth/callback instead of https://myapp-git-feature-team.vercel.app/auth/callback.
How Supabase redirect URLs work
Supabase maintains an allowlist of permitted redirect destinations. When you pass a redirectTo in your auth calls, Supabase validates it against the list. If the URL is not in the list, the request fails.
// This redirectTo will fail if the current preview URL is not in the allowlist
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 — which does not match any pattern in the list.
The dashboard config
Location: Supabase Dashboard → Authentication → URL Configuration
You need three things configured:
Site URL (single value):
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):
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.
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 filename="lib/utils.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 filename="app/auth/actions.js"
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 (data.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 |
NEXT_PUBLIC_VERCEL_URL is automatically set by Vercel on every deployment with the correct preview URL. You do not need to set it yourself — but you do need NEXT_PUBLIC_SITE_URL in production to override the Vercel URL with your custom domain.
Email templates: the hidden SITE_URL problem
Password reset and magic link emails use your SITE_URL by default in the template:
<!-- Default template — uses SITE_URL, ignores redirectTo -->
<a href="{{ .SiteURL }}/auth/confirm?...">Confirm your email</a>
If your SITE_URL is http://localhost:3000 (the Supabase default before you change it), production password reset emails will send users to localhost.
The fix: update your email templates to use {{ .RedirectTo }} instead of {{ .SiteURL }}:
<!-- Updated template — uses dynamic redirectTo -->
<a href="{{ .RedirectTo }}">Confirm your email</a>
Location in Supabase Dashboard: Authentication → Email Templates
The trailing slash trap
Supabase URL matching is exact. https://yourdomain.com and https://yourdomain.com/ are different entries.
If your redirectTo is https://yourdomain.com/auth/callback and your allowlist only has https://yourdomain.com (no trailing slash, no wildcard), it will fail.
The /** wildcard suffix is the safest way to avoid this: https://yourdomain.com/** matches any path on that domain including /auth/callback.
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 the email template using {{ .SiteURL }} instead of {{ .RedirectTo }} — not the allowlist. Allowlist errors return an explicit error. Wrong-URL-in-email is silent: the user just lands somewhere unexpected.
If window.location.origin in your getSiteURL() helper returns undefined or an unexpected value server-side (it is a browser API), that is why — use NEXT_PUBLIC_SITE_URL or NEXT_PUBLIC_VERCEL_URL env vars instead, which work on the server.
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. - [ ] Email templates updated to use
{{ .RedirectTo }}not{{ .SiteURL }}. - [ ] Test: open a fresh Vercel preview URL, click "Forgot password", verify email link goes to the preview URL (not production).
Related
- Supabase auth redirect fix — related redirect issues with email confirmation and OAuth.
- Supabase email confirmation fix — email template issues that break the confirmation flow.
- Magic link production checklist — covers the SITE_URL and email template setup for magic link auth.
- Deploying Next.js and Supabase to production — full production setup including environment variables per environment.
Originally published at https://www.iloveblogs.blog
Top comments (0)