v0 auth not working: why login breaks between preview and production
Most v0 auth failures are environment problems, not form problems. The v0 preview only reads Development variables, Supabase does not know your preview or production URLs, Vercel protection sits in front of your login, or the session refresh file never runs. Fix those four, then check the session on the server in every protected page and action.
Symptom
I could not find an official error message for these cases in v0's or Vercel's docs, so I describe the symptom rather than quote one. The variants I check for first:
- Works in the chat preview, fails on the
vercel.appor custom domain after Publish. - Works on production, fails on a branch preview.
- Confirmation or magic-link emails send people to the wrong address.
- Logged in on one page, logged out on the next, or randomly logged out after a while.
Why it happens
v0 builds Next.js apps that run in a Vercel Sandbox for the preview and on Vercel for preview and production deployments. Auth depends on all three environments agreeing on keys, URLs and cookies. That is where it breaks.
The preview and production read different variables
v0 does not store your keys itself. Its docs say environment variables are inherited from the connected Vercel project, and that the v0 preview window can only access variables from the Development environment. The same page says sensitive environment variables are inaccessible to v0 previews.
So a key you set for Production only is invisible in the chat preview, and a key you set for Development only is missing once you publish. Vercel also states that changes to variables only apply to new deployments, so editing a value without publishing again changes nothing on the live site.
Supabase does not know your URLs
Every working branch in a GitHub-backed v0 project gets its own preview deployment with a unique URL, and production has a stable URL of its own. Supabase sends people back to the Site URL by default when no redirect is specified, and expects preview patterns such as https://*-<team-or-account-slug>.vercel.app/** to be added as extra redirect URLs. If the Site URL is still http://localhost:3000, your emails point there.
Vercel is asking for a login before your app does
Vercel's Standard Protection protects every deployment except production domains, and the Vercel Authentication method only lets in Vercel users with access to the project. If a tester sees a Vercel sign-in on a preview URL, that is deployment protection, not your auth. You choose visibility in the first Publish flow and later under Project menu then Settings then Vercel Project.
The session refresh file never runs
Supabase's server-side auth for Next.js refreshes the session in a proxy file. Its guide says the file is proxy.ts on Next.js 16 and later, and that on Next.js 15 and earlier a proxy.ts file is never called; there it must be middleware.ts exporting middleware. A file with the wrong name for your version does nothing, and users get logged out when their token expires.
Old generations still contain old auth
v0's database docs note that once an integration is set up, newer generations in the project can use the connected database. Pages generated before you connected Supabase may still hold placeholder sign-in logic. Search for it before assuming the integration is at fault.
The fix
-
Match the variable names to the code. Open Project menu then Settings then Environment Variables and list every auth variable with its environments. Supabase's current Next.js guide reads
NEXT_PUBLIC_SUPABASE_URLandNEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY. The Marketplace integration may have added keys with different names, so compare them with what your code reads, and make sure each one is ticked for Development (preview) and Production (live). Then publish again so a new deployment picks them up. -
Add every URL to Supabase. Set the Site URL to your production domain. Add
http://localhost:3000/**and the Vercel preview pattern as redirect URLs. Then pass an explicit redirect from the code, using the helper shape from Supabase's docs:
// lib/site-url.ts
export function getURL() {
let url =
process.env.NEXT_PUBLIC_SITE_URL ?? // set this to your production domain
process.env.NEXT_PUBLIC_VERCEL_URL ?? // set automatically by Vercel
"http://localhost:3000/";
url = url.startsWith("http") ? url : `https://${url}`;
return url.endsWith("/") ? url : `${url}/`;
}// app/login/actions.ts
"use server";
import { createClient } from "@/lib/supabase/server";
import { getURL } from "@/lib/site-url";
export async function sendMagicLink(formData: FormData) {
const email = String(formData.get("email") ?? "");
const supabase = await createClient();
const { error } = await supabase.auth.signInWithOtp({
email,
options: { emailRedirectTo: `${getURL()}auth/callback` },
});
return { ok: !error };
}-
Test login where protection is off. Under Standard Protection the production domain is public, so test sign-in there or on your custom domain. If you need public previews for testers, change visibility rather than switching auth off in the code.
-
Name the session file for your Next.js version. Check
nextinpackage.json. On 16 or later, useproxy.tswithexport async function proxy. On 15 or earlier, rename itmiddleware.tsand exportmiddleware. The helper itself is the same:
// proxy.ts (Next.js 16+). On Next.js 15, name it middleware.ts and export `middleware`.
import { type NextRequest } from "next/server";
import { updateSession } from "@/lib/supabase/proxy";
export async function proxy(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};Supabase's own updateSession helper creates a server client, calls supabase.auth.getClaims() straight away and returns the response it built. Copy it from their example rather than letting the model write its own, and do not add code between creating the client and that call.
- Check the user on the server, next to the data. Next.js says the proxy should not be your only line of defence and that Server Actions deserve the same care as public API endpoints. Supabase also warns never to trust
getSession()in server code. One helper keeps that consistent:
// lib/auth/require-user.ts
import "server-only";
import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
export async function requireUser() {
const supabase = await createClient();
const { data, error } = await supabase.auth.getUser(); // network call to Supabase Auth
if (error || !data.user) redirect("/login");
return data.user;
}Call requireUser() at the top of every protected page, Server Action and Route Handler, and filter queries by user.id.
How to prevent it
- Save a v0 Instruction for auth work. Instructions live in the + menu of the prompt bar under New Instruction and apply to any chat while checked. Mine would say: use
@supabase/ssr, callrequireUser()in every Server Action, never read secrets in client components. - Keep the redirect URL list in Supabase in step with your domains. Every new custom domain needs an entry.
- Test sign-up, sign-in, sign-out and password reset on a branch preview before you select Publish, then once more on production.
- Use row level security on every table the app reads, so a missed server check does not expose other users' rows.
For the wider pattern behind these failures, see my guide to fixing auth in AI-built apps, and the checklist for deploying a v0 app to production, which covers the variable scoping in more detail. If you also edit the code in Cursor, the Cursor auth article covers the same problem from the editor side.
When to get help
If sign-in works but you are not sure every page and action checks the user, that is the part that leaks data, and it is hard to see from the preview. Run the free vibe code health check first. If it flags auth, a Rescue Sprint is a fixed-price audit and fix, usually about a week. The guide to fixing vibe-coded apps shows where auth sits among the other common failures.
- v0 docs: Vercel Integration (environment variables and previews), checked 10 November 2026
- v0 docs: External APIs (environments table), checked 10 November 2026
- v0 docs: Sandbox (preview runs on Vercel Sandbox), checked 10 November 2026
- v0 docs: Deployments, checked 10 November 2026
- v0 docs: Databases, checked 10 November 2026
- v0 docs: Instructions, checked 10 November 2026
- Vercel docs: Environment variables, checked 10 November 2026
- Vercel docs: Deployment Protection, checked 10 November 2026
- Supabase docs: Redirect URLs, checked 10 November 2026
- Supabase docs: Server-side auth for Next.js, checked 10 November 2026
- Supabase example: Next.js proxy.ts, checked 10 November 2026
- Next.js docs: Authentication, checked 10 November 2026
The vibe-code rescue checklist, as a PDF
About 40 checks across auth, data, payments, errors, mobile and secrets, on two pages.
Questions founders ask
Why does login work in the v0 preview but not after I publish?
The preview reads variables from the Development environment of the linked Vercel project, while the published app reads Production. If a key only exists in one, or Supabase only allows one URL, login works in one place and fails in the other.
Do I need middleware.ts or proxy.ts for Supabase auth in a v0 app?
It depends on the Next.js version in package.json. Supabase's guide uses proxy.ts on Next.js 16 and later, and middleware.ts on Next.js 15 and earlier, where a proxy.ts file is never called.
Is protecting pages in the proxy enough?
No. Next.js says the proxy should not be your only line of defence. Check the user again in the page, Server Action or Route Handler that reads or changes data.

Ant
Product designer who builds MVPs, based in Nottingham. 10+ years designing digital products, and the only person at BlackFig. More about me