v0 app not working after deploying to Vercel: fix guide
You built the app in v0. In the v0 preview everything works: the dashboard loads, the form saves, the AI feature answers. Then you click Publish and v0 deploys it to Vercel, and the live site is different. The page flashes and re-renders, the console shows Minified React error #418, a form submit fails with "An error occurred in the Server Components render", or a value that was fine in the preview is suddenly undefined.
Almost every case falls into one of three groups: hydration mismatches, Server Actions that fail in production, and environment variables that exist in the preview but not in Production. This guide explains why each one only shows up after deploy and gives the exact fix. If the Vercel build itself fails (red build log), start with Vercel build failed: Module not found instead.
Why the v0 preview and Vercel production behave differently
The preview is not a smaller copy of your production site. A few differences explain most "works in v0, broken on Vercel" reports:
| In the v0 preview | In production on Vercel |
|---|---|
| Runs your app in a development-style mode, rendered on request. | Runs a next build output: pages without dynamic data are prerendered once at build time and served from cache. |
| Reads only the Development environment variables of the connected Vercel project (variables marked sensitive are not available to it). | Reads the Production variables, and only those that existed when the deployment was built. |
| Shows full React and Next.js error messages. | Minifies React errors and hides Server Component error messages behind a digest. |
| Usually one long-running dev server, so data kept in memory seems to persist. | Server code runs as Vercel Functions: many short-lived instances, a read-only file system except /tmp, and the server clock in UTC. |
Step 0: get the real error before changing code
- Browser: open the live URL, press
F12, reload, and copy the first red Console error plus any failed request in the Network tab. - Server: in the Vercel dashboard open the project, then Logs (runtime logs). Filter by the failing path or by status 500. Server Action and Server Component errors show their full message and stack trace here, even though the browser only sees a digest.
- Reproduce production locally: sync the v0 project to GitHub or download the code, then run a production build instead of the dev server. Hydration and caching problems often appear only in this mode.
npm install # connect to the same Vercel project and pull its Production variables npx vercel link npx vercel env pull .env.local --environment=production # production build, served on http://localhost:3000 npm run build && npm start
Treat the pulled .env.local as a secret file: it is git-ignored by default in Next.js projects, keep it that way, and delete it when you're done if it holds production keys.
| What you see | Most likely cause | Go to |
|---|---|---|
Hydration failed because the server rendered HTML didn't match the client, Minified React error #418, content flicker on load | Something renders differently on the server and in the browser | Part 1 |
| Form submit does nothing or shows "An error occurred in the Server Components render… digest" | A Server Action throws in production | Part 2 |
Failed to find Server Action | The page was loaded from an older deployment | Part 2, fix 4 |
process.env.X is undefined, supabaseUrl is required, 401 from an API | Variable missing from Production, not redeployed, or wrong prefix | Part 3 |
Part 1: Hydration mismatch errors
Next.js renders each page to HTML on the server, then React "hydrates" it in the browser by rendering the same components again and attaching event handlers. If the browser render produces different markup, React reports a hydration error. In development you see the readable message, for example Hydration failed because the server rendered HTML didn't match the client (React 19) or Text content does not match server-rendered HTML (React 18). In production you get a minified error such as #418 (and #423 / #425 on React 18), and React falls back to re-rendering on the client, which is the flash you see.
To find the component, run the production build locally (Step 0) or npm run dev: the dev error overlay shows a diff of the server and client markup.
1. Dates, times and random values rendered on the server
Signature: the mismatch is in a timestamp, a "x minutes ago" label, a greeting like "Good morning", or a generated id.
Code like new Date().toLocaleString(), Date.now(), Math.random() or crypto.randomUUID() inside a component gives a different result on each render. On Vercel it is worse than on your laptop: the server renders in UTC (or at build time, for a prerendered page) while the visitor's browser uses their own time zone and locale.
Fix: render a stable value on the server and the local value only after the component has mounted.
'use client'
import { useEffect, useState } from 'react'
export function LocalTime({ iso }: { iso: string }) {
const [label, setLabel] = useState(iso.slice(0, 10)) // same on server and client
useEffect(() => {
setLabel(new Date(iso).toLocaleString()) // browser-only, after hydration
}, [iso])
return <time dateTime={iso}>{label}</time>
}
Alternatively format with an explicit time zone and locale, for example new Intl.DateTimeFormat('en-US', { timeZone: 'UTC' }), so both sides produce the same string. For element ids, use React's useId() instead of random values.
2. Browser-only APIs read during render
Signature: the component uses window, localStorage, matchMedia or navigator, or branches on typeof window !== 'undefined'.
Even with 'use client', Client Components are rendered on the server first. A check like typeof window !== 'undefined' ? savedCart : [] returns different markup on the server and in the browser. Read browser-only values in useEffect and store them in state, exactly like the date example above.
For a whole widget that can't render on the server (some charts, maps or editors), load it client-only. In the App Router, ssr: false is only allowed inside a Client Component:
'use client'
import dynamic from 'next/dynamic'
const Map = dynamic(() => import('./map'), { ssr: false, loading: () => <p>Loading map…</p> })
export function MapSection() {
return <Map />
}
3. Theme switching (dark mode)
Signature: the mismatch is on the <html> element's class or style, or on a sun/moon icon.
v0 apps often use shadcn/ui with next-themes, which sets the theme class on <html> before React hydrates. That is expected, and next-themes documents adding suppressHydrationWarning to <html> in app/layout.tsx:
<html lang="en" suppressHydrationWarning>
A theme toggle that shows a different icon for each theme should render a neutral placeholder until mounted, because the server can't know the visitor's theme.
4. Invalid HTML nesting
Signature: the dev message mentions that an element "cannot be a descendant of" or "cannot contain a nested" another element.
When the HTML is invalid, the browser repairs it while parsing, so the DOM no longer matches what React expects. Common cases in generated code: a <div> (or a component that renders one) inside a <p>, a link inside a link, a button inside a button, or <tr> directly inside <table> without <tbody>. Change the outer element (for example <p> to <div>) or use the component's asChild prop where shadcn/ui offers it instead of nesting two interactive elements.
5. Browser extensions
Signature: the diff shows attributes your code never sets, often on <body> or <html>, and the error disappears in an incognito window with extensions disabled.
Password managers, grammar checkers and translators edit the page before React hydrates. That is not a bug in your app. Confirm it in incognito before changing any code.
Don't spray suppressHydrationWarning everywhere to silence real bugs. It only applies one level deep, to the element's own attributes and text, and it hides the warning without fixing the mismatch.
Part 2: Server Actions failing in production
v0 generates Next.js Server Actions (functions marked 'use server') for forms and mutations. In production, if one throws, the browser only receives a generic message: "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance…" The real error is in Vercel Logs (Step 0). Find it there first. Then make failures visible without leaking details:
'use server'
export async function createTask(formData: FormData) {
try {
// ...validate input, check the user's session, write to the database
return { ok: true }
} catch (err) {
console.error('createTask failed', err) // full error goes to Vercel Logs
return { ok: false, error: 'Could not save the task. Please try again.' }
}
}
1. A server-side environment variable is missing
Signature: the log shows DATABASE_URL, an API key or a client constructor complaining about an undefined value.
Server Actions run with your Production variables. If the key only exists in Development (which is all the v0 preview can see), the action works in the preview and fails live. See Part 3.
2. Data saved in memory or on the file system
Signature: new records appear and then vanish, differ between page loads, or the log shows EROFS: read-only file system.
Prototypes generated by AI tools sometimes keep data in a module-level array (let tasks = []) or write a JSON file. That can look fine in a single preview process. On Vercel, each function instance has its own memory, instances are started and recycled at any time, and the deployment's file system is read-only (only /tmp is writable, and it is temporary and per instance). Move the data to a real database or storage service, for example Supabase, Neon, Upstash or Vercel Blob, which you can connect from v0 or the Vercel Marketplace.
3. The action works but the page shows old data
Signature: the write succeeds (you can see it in the database) but the list on the page doesn't change until much later or until the next deploy.
In production, a page that doesn't use request data is prerendered at build time and served from cache. After a mutation, tell Next.js which page or data to refresh:
'use server'
import { revalidatePath } from 'next/cache'
export async function createTask(formData: FormData) {
// ...insert the task
revalidatePath('/tasks')
}
For a page that must always render fresh per-user data, you can also opt it out of static rendering with export const dynamic = 'force-dynamic' in that page file. Reading cookies() or headers() makes a page dynamic automatically.
4. "Failed to find Server Action"
Signature: the error says the request "might be from an older or newer deployment", usually right after you published a new version.
Server Action ids change between builds. A browser tab that loaded the old version still calls the old id. A refresh fixes it for that visitor. On Vercel, Skew Protection keeps the previous deployment reachable for clients that loaded it; check Settings → Advanced → Skew Protection (it depends on your plan and is on by default for newer projects). In the UI, treat a failed action as "please refresh and try again" rather than a dead end.
5. Uploads larger than the limits
Signature: small files upload but bigger ones fail, with Body exceeded 1 MB limit in the logs or a 413 FUNCTION_PAYLOAD_TOO_LARGE response.
Server Actions accept 1 MB request bodies by default. You can raise that in next.config:
// next.config.mjs
const nextConfig = {
experimental: {
serverActions: { bodySizeLimit: '4mb' },
},
}
export default nextConfig
Vercel Functions also have a hard 4.5 MB request body limit, so for images, PDFs or video, upload from the browser directly to storage (Vercel Blob client uploads, or a Supabase Storage signed upload URL) and pass only the resulting URL to the action.
6. Timeouts on slow work
Signature: the action works for short inputs but long ones fail with 504 / FUNCTION_INVOCATION_TIMEOUT, typical for AI generation, scraping or large imports.
Every function has a maximum duration that depends on your plan and settings. You can raise it for one page or route, within your plan's limit, with export const maxDuration = 60 (seconds) in that file. For long AI responses, stream the output from a Route Handler instead of waiting for the whole result in one action. For jobs that take minutes, use a queue or background job instead of a request.
7. The edge runtime and Node-only packages
Signature: errors such as The edge runtime does not support Node.js 'crypto' module or a database client that refuses to run on edge.
If a page or layout has export const runtime = 'edge', its actions run on the edge runtime, which lacks many Node.js APIs. Database drivers, bcrypt, fs and many SDKs need Node.js. Unless you chose edge on purpose, remove that line so the default Node.js runtime is used.
While you are in your actions: each Server Action is a public POST endpoint. Check the user's session and permissions inside every action, not only in the UI that calls it.
Part 3: Environment variables missing or undefined after deploy
A v0 chat uses the environment variables of its connected Vercel project. When you connect an integration (Supabase, Neon, Upstash and others) from v0, its variables are added to that project for you. The catch is the environment scope: the preview reads Development variables, your live site reads Production variables, and a variable can exist in one but not the other.
1. The variable isn't enabled for Production
In the Vercel dashboard go to your project, then Settings → Environment Variables. For each variable your code uses, check that Production is ticked (and Preview, if you use preview deployments). A key added only for Development works in the v0 preview and is undefined live.
2. The variable was added or changed, but nothing was redeployed
Environment variable changes apply only to new deployments. After adding or editing a variable, redeploy: publish again from v0, or in Vercel open Deployments, choose the latest production deployment and select Redeploy. This matters doubly for NEXT_PUBLIC_ variables, because their values are written into the JavaScript bundle at build time.
3. Browser code reads a variable without the NEXT_PUBLIC_ prefix
In Next.js, only variables whose names start with NEXT_PUBLIC_ are available in the browser. In a Client Component ('use client'), process.env.SUPABASE_URL is undefined by design. Decide which kind of value it is:
- Public by nature (Supabase project URL and anon key, Stripe publishable key, analytics id): rename it to
NEXT_PUBLIC_…in both the code and Vercel, then redeploy. - Secret (OpenAI or other AI keys, Stripe secret key, Supabase
service_role, database URLs, email API keys): keep the name without a prefix and move the code that uses it to the server: a Server Action, a Route Handler inapp/api/…/route.ts, or a Server Component.
4. Never "fix" a secret by adding NEXT_PUBLIC_
Renaming OPENAI_API_KEY to NEXT_PUBLIC_OPENAI_API_KEY makes the error go away because the key is now copied into a public JavaScript file that anyone can download. If that has already been deployed, renaming it back is not enough: rotate the key at the provider (revoke it and create a new one), store the new one without a prefix, and redeploy.
To stop server-only code from being imported into a Client Component by accident, add import 'server-only' at the top of files that use secrets (after npm install server-only). The build then fails if a client file imports them.
5. The variable is looked up dynamically
Next.js replaces process.env.NEXT_PUBLIC_API_URL with its value at build time, but only when written out literally. Lookups like process.env[name] or const env = process.env; env.NEXT_PUBLIC_API_URL in browser code are not replaced and stay undefined. Write each public variable out in full.
6. The names don't match
The code says NEXT_PUBLIC_SUPABASE_URL, Vercel has SUPABASE_URL, or a value was pasted with quotes or a trailing space. List what the code expects and compare it with the dashboard:
grep -rhoE "process\.env\.[A-Z0-9_]+" app components lib --include=*.{ts,tsx,js,jsx} | sort -u
Adjust the folder list to your project, and remember .env.local is never deployed. For the full cross-platform checklist, see Environment variables missing in production.
A pre-publish checklist for v0 apps
npm run build && npm startworks locally with production variables, and the browser console is clean.- No dates, random values or
window/localStoragereads in the first render;suppressHydrationWarningonly on<html>for the theme. - Every variable from the grep exists in Vercel with Production ticked; redeployed after the last change.
- Only genuinely public values use
NEXT_PUBLIC_; any secret that was ever public has been rotated. - Data lives in a database or storage service, not in memory or local files.
- Mutations call
revalidatePathorrevalidateTag; per-user pages are dynamic. - Uploads go straight to storage; slow work streams or runs as a background job.
- Each Server Action checks the session, returns a friendly error and logs the real one.
- Tested in an incognito window on the live URL: sign up, log in, create, edit, delete.
How to prompt v0 so it fixes the right thing
The deployed app at https://myapp.vercel.app fails, but the v0 preview works. Vercel runtime log for POST /tasks: Error: DATABASE_URL is not defined (lib/db.ts:4) Do NOT change UI or refactor. Explain the cause in 2 sentences, then propose the smallest fix. If it is a Vercel setting, say so and change no code.
Always paste the real log line. If the cause is a dashboard setting (variable scope, redeploy, Skew Protection), no code change will fix it, and asking v0 to "fix the deploy" can rewrite working code.
FAQ
Why does my v0 app work in preview but not after deploying to Vercel?
The v0 preview runs in a development-style mode and reads only the Development environment variables of the connected Vercel project. Production runs a next build output: pages are prerendered and cached, errors are minified, server code runs as short-lived Vercel Functions in UTC, and only Production variables are used. Hydration mismatches, failing Server Actions and missing Production env vars are the usual results.
How do I fix a hydration error in a v0 Next.js app?
Find the component that renders differently on the server and in the browser: dates, Math.random(), window or localStorage reads during render, theme classes, or invalid HTML nesting like a div inside a p. Render a stable value first and the browser-only value in useEffect, load browser-only widgets with next/dynamic and ssr: false inside a Client Component, and add suppressHydrationWarning only to the html element for next-themes.
Why do my Server Actions fail only in production?
In production the browser only gets a digest, so read the real error in Vercel Logs. Common causes are a server-side env var missing from Production, data kept in memory or written to the read-only file system, missing revalidatePath after a mutation, a stale tab from an older deployment, uploads over the 1 MB Server Action or 4.5 MB Vercel limit, timeouts, and Node-only packages on the edge runtime.
Why are my environment variables undefined on Vercel?
The variable is not enabled for the Production environment, it was added without redeploying, browser code reads a name without the NEXT_PUBLIC_ prefix, it is looked up dynamically instead of written out as process.env.NEXT_PUBLIC_NAME, or the name in the code does not match the name in Vercel.
Can I add NEXT_PUBLIC_ to an API key so it works in the browser?
Only for values that are public by design, such as a Supabase anon key or a Stripe publishable key. Never for secrets like OpenAI, Stripe secret or Supabase service_role keys: NEXT_PUBLIC_ values are copied into public JavaScript. Keep secrets server-side in a Server Action or Route Handler, and rotate any secret that was already deployed with the prefix.