Someone forwarded me a v0 link last month with one line: "customers start using this Friday, can you look tonight." Four hours earlier, it hadn't existed. That's not marketing copy, that's what v0 actually does, type a prompt, get a working Next.js app with a database wired in and a live Vercel URL. I've been doing this job twenty years and I still catch myself blinking at how fast that step happens now.
Most of what lands on my desk these days got built this way, by some AI tool, days or hours before someone tries to put real users on it. Lovable, Bolt, Replit, Base44, Cursor, v0. v0 sits a little apart from that list though. It doesn't run on its own hosting the way Lovable and Bolt do. It ships straight to Vercel, the same infrastructure running a huge slice of the production web. So "is v0 safe" is actually two questions stacked on top of each other: is the code it writes safe, and is the platform it lands on safe. Different answers, and worth pulling apart before you tell anyone the link is live.
Where v0 actually gets this right
Vercel is not a sketchy host. Deploys are real, versioned, and instantly rollback-able, I've used that rollback button myself more than once on client work that had nothing to do with AI-generated code. Preview URLs get their own environment per branch, which is a genuinely good default most hand-rolled CI setups still don't bother with. And because v0 outputs actual Next.js, App Router, Server Components, the works, you're not stuck with some proprietary export format if you ever want a real engineer to take over the codebase. That part of the pitch holds up. None of what follows is a knock on Vercel's platform.
Three gaps, and they're not about the platform
The env var that rides straight into the browser. Next.js has one rule everyone learns the hard way eventually: prefix a variable NEXT_PUBLIC_ and it gets inlined into the client bundle at build time, no exceptions. v0 knows this rule. What it doesn't reliably know is which of your keys should carry that prefix and which never should. I've opened v0-generated client components pulling a variable that had no business being reachable from a browser, because somewhere in the chat history that felt like the fastest way to make the fetch call work. Once it's baked into the bundle, it's live for every visitor, and rotating the key after the fact doesn't un-ship the copies already cached.
The service role key sitting one line below the public one. Connect Supabase through the Vercel Marketplace, the flow v0 pushes you toward, and it writes six environment variables in one pass: SUPABASE_URL, NEXT_PUBLIC_SUPABASE_URL, SUPABASE_ANON_KEY, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, and SUPABASE_JWT_SECRET. Two of those are meant for the browser. Four are absolutely not, and SUPABASE_SERVICE_ROLE_KEY specifically bypasses every row-level security policy on your database, by design, that's its entire job. It sits in the same list, same casing convention, one scroll away from the anon key. I've seen it get pulled into a server action that never needed it, which isn't a leak yet, but it's one lazy refactor away from becoming the worst version of the anon-key problem instead of the ordinary one.
The server action nobody's guarding. v0 leans hard on Next.js Server Actions for anything that writes data, a form submit, an update, a delete. That's idiomatic Next.js, nothing wrong with the pattern itself. The part that surprises people: a Server Action marked 'use server' compiles into a real POST endpoint the instant you export it, whether or not you ever call it from your UI. Next.js's own docs say to treat it like a public API route. v0 will happily generate the action and the button that calls it. It won't reliably add the session check inside that action confirming the caller is who they claim to be, because the demo works fine without one. Anyone who opens dev tools, copies the request, and replays it with a different user's ID gets the same result the button did.
If your v0 app runs on Supabase, the RLS gap covered in the exposure report applies here too, full stop. It's the same schema-first-policy-later habit every AI builder shares, v0 included.
What the wider data says
I haven't scanned a v0-only sample yet, the tool's newer to the full-stack builder space than Lovable or Bolt. But the pattern behind it isn't new. The audit findings post walks through what showed up in every one of the first 20 apps I reviewed, regardless of which tool built them, and the exposure report put a number on it: 41% of the Supabase-backed apps in a 66-app sample had at least one table any stranger could read with nothing but the public key. Is Replit safe covers the same three-gap shape from a different builder's angle, visibility, secrets, auth, worth a look for the comparison. The mechanism keeps repeating because the demo works either way. That's the whole problem in one sentence.
The two-minute self check
Three checks. No scanner required, just dev tools and a terminal.
# 1. Search your own client bundle for anything that shouldn't be there
# In the browser, view-source or open the Network tab, load your app,
# search the JS payload for "service_role", "secret", or any key name
# you didn't intend to expose.
# 2. Is a database table readable with just the anon key?
curl "https://<your-project>.supabase.co/rest/v1/<table_name>?select=*&limit=1" \
-H "apikey: <your-anon-key>"
# 3. Does a server action check who's calling before it runs?
# Copy the request as curl from devtools' Network tab, look for a
# POST request with a next-action header, swap the session cookie
# for none, or for a different user's, and run it again.
Anything sensitive turning up in check one means it already shipped to every visitor who loaded that page. Rows on check two mean that table has no policy behind it. The same result on check three with a missing or swapped session means the action trusts the request more than it verifies the person making it.
If you find something
Rotate any key that showed up in check one immediately. Treat it as burned the moment it was reachable, whether or not you can prove someone read it. For the database, turn on RLS and scope it to the owner, auth.uid() = user_id covers most tables. For the server action, add the session check inside the action itself, not in the button that calls it, then rerun check three and confirm it now fails cleanly instead of executing.
Where this leaves v0
v0 didn't invent any of these three gaps, and it's not worse than Lovable or Bolt on the database layer, they all inherit the same Supabase default the moment RLS enters the picture. What's specific to v0 is the surface area that comes with writing real Next.js: real env var rules, real Server Actions, real Vercel environment scoping across preview and production. More power, more places for a key to end up somewhere it shouldn't. Run the three checks above first, they're free, and they're the exact test behind everything in this post. If you'd rather have someone run those plus the storage-bucket and cross-environment checks a quick pass tends to miss, the Vibe Check Launch Risk Review at uxcontinuum.com/vibe-check is one option, a fixed-price overnight pass with a ranked report, not a sales call.
One flag: the cornerstone link (/blog/ai-built-app-audit-findings) doesn't exist yet in the uxcontinuum repo, only the messy draft is-replit-safe.mdx references it. And that draft file has a problem worth your attention: it's got a prior session's meta-commentary ("One thing worth flagging first...") written directly into the file content instead of just the post body, so it isn't ready to ship as-is. Neither issue is something I touched, just wanted you to see it before either post goes live.