Skip to content

Is your v0 app safe?

Is v0 safe to build with? The generated components are not the risk. The wiring you add around them is, and one variable name decides most of it.

Vlad Tkachenko4 min read
Reeve's security-check card for v0 apps, with the v0 logo on a white tile.

Logos are the property of their respective owners, shown to indicate compatibility.

In short

  • A v0 app is usually safe to ship; the generated components are not the risk. The wiring you add around them is.
  • Any variable named NEXT_PUBLIC_… is compiled into the JavaScript your visitors download. That is right for a publishable key and wrong for a secret one.
  • v0 does not set your database rules. On Supabase, tables created by running SQL start without Row Level Security.

v0 gives you components: a working interface, generated and dropped into your project. What it does not give you is the wiring behind them: the environment variables, the database, the rules about who may read what. That gap is where a v0 project usually goes wrong, and it goes wrong quietly.

Here is the part that guide after guide gets wrong: the generated code is not the risk. The risk is one naming convention in your environment file, and a database setting that has nothing to do with v0 at all.

Which half of a v0 project is public

The interface half, entirely.

Think of it as a shop. The components v0 writes are the shopfront (every page, every form, every button), and the whole shopfront is handed to each visitor who loads your site. There is no way around that; a browser cannot render a page it was not sent. Behind it is the stockroom, your database, which is a separate building with its own lock.

What makes a v0 project distinctive is that you assemble the two yourself. The components arrive knowing nothing about your keys or your tables, so every decision about what crosses from stockroom to shopfront is one you make in your own project, usually in a single file full of environment variables.

What NEXT_PUBLIC_ really means

It means "put this in the browser".

Next.js, which is what v0 generates for, decides what your visitors receive by reading the variable's name. Anything named NEXT_PUBLIC_SOMETHING is compiled into the JavaScript your site serves. Anything without that prefix stays on the server. The same rule exists under other names in other tools (Vite uses a VITE_ prefix and states plainly that those values are bundled into your source code at build time), but the effect is identical: the prefix publishes the value.

That is exactly right for a project URL or a publishable key. It is exactly wrong for a secret one.

Both are called API keys. Only one of them was ever meant to be readable.

The trap has a recognisable shape. Something in the interface cannot read a value, so the prefix gets added to make the error go away, and it does: the app starts working. If the value was a Supabase sb_publishable_… key, or the older anon key, nothing is wrong. If it was sb_secret_… or service_role, the error was the system telling you that code belongs on a server, and the rename published a key that ignores every rule you have set. Telling the two apart takes about a minute and is worth doing once for every key in the file.

What decides whether strangers can read your data

Your database rules, not your components.

Your app and a stranger knock on the same door. The setting decides who gets in, not your app's screens.

If your data is in Supabase, the setting is Row Level Security: a per-table switch that decides row by row who may read what. With it off, your publishable key returns the whole table to anyone who asks. With it on and a policy written, it returns only what the policy allows.

Supabase enables it by default for tables created in the dashboard's Table Editor, and not for tables created by running SQL. So the tables you clicked into existence are usually protected, and any table created by a migration file may not be. Both look the same afterwards, and both work.

There is a second version of this that catches people who did everything right: a table can have Row Level Security enabled and still be open to everyone, because of the policy on it. Enabled and protected are two different states.

How to check your own v0 project in about ten minutes

Read your environment file line by line. Every variable with a NEXT_PUBLIC_ prefix is published. Ask of each one: would I be comfortable printing this on the homepage? If not, it does not belong with that prefix.

Rotate anything that should not have been there. In your provider's dashboard, generate a new key and revoke the old one. Removing the line from your code does not close the door, because the old value is still in your version history and in anyone's cached copy of the site.

Check your table rules. In Supabase, Authentication → Policies lists every table and whether Row Level Security is on. Anything disabled is readable by anyone with your project address.

Then look from outside. The steps above tell you what is configured; they do not tell you what is actually reachable. Our free scan reads your live site the way a visitor would and gives you a grade in about 20 seconds, with no account: scan your app.

What to do

  • NEXT_PUBLIC_ is not a formality. It compiles the value into the files every visitor downloads.
  • Adding the prefix to silence an error is the moment to stop and check what the value actually is.
  • A publishable key belongs in the browser. sb_secret_… and service_role never do.
  • v0 does not set your database rules. On Supabase, tables created by running SQL start without Row Level Security.
  • A table with Row Level Security enabled can still be open to everyone. The policy is the part that decides.

Open your environment file and read the NEXT_PUBLIC_ lines out loud. If one of them holds something you would not print on your homepage, rotate it now. Then the 10-minute security checklist covers what else is worth switching off in a freshly launched project.

What can actually go wrong with a v0 app

None of these are your fault. They come with generating code fast. Here's what's worth checking:

  • A secret key in a client component (the NEXT_PUBLIC_ trap)

    v0 builds with Next.js, which has a rule that trips people up: anything named NEXT_PUBLIC_, and any key written straight into a client component, gets sent to the browser, where anyone can read it. It's easy to paste an API key into a generated component without realising it's now public. Some keys are meant to be public; Reeve tells those apart from the ones that aren't, so no false alarms.

  • Your database left open (Supabase RLS off)

    If your v0 app stores data (often in Supabase), there's a switch called Row Level Security (RLS) that decides who can read or change each row. If it's off, your tables can be open to anyone who finds the address. It's the most common serious issue in generated apps and stays invisible until you look.

  • Published source maps

    A "source map" reveals your app's original code to anyone who opens the browser tools. It's helpful while building, but if it ships to production it hands strangers a readable copy of how your app works, and makes every other gap easier to find. Reeve checks whether yours are exposed.

  • Open API routes

    Next.js apps often include API routes: small endpoints that do things like read or write data. If one was generated without an auth check, it may answer to anyone who calls it. Reeve probes whether your endpoints respond to strangers, without ever using them to change anything.

  • An exposed .env or config file

    The .env file holds a project's keys. Occasionally it gets published with the deployed app by mistake, and if it's reachable it's a shortcut to everything sensitive. Reeve checks whether yours is quietly accessible.

  • Missing safety headers & open sharing (CORS)

    Small settings that tell browsers how to protect visitors, and whether any website is allowed to call your app's data. Each one is minor on its own, but together they widen the gap. Reeve flags what's missing.

What Reeve is, and what it isn't

Reeve is a free, read-only check from the outside, like an inspector trying the doors, not stepping inside. It catches the common, high-impact mistakes fast. It is not a full security audit, and a clean grade isn't a guarantee; it means the obvious doors are shut.

v0 and Vercel keep raising the quality of what's generated and deployed, and if you use Supabase, its own advisor flags database issues in the dashboard. Both help. What Reeve adds: you're working in v0, not reading the generated code line by line, and those tools speak developer. Reeve looks at the whole deployed app from the outside and tells you what it finds in plain words. And if you'd rather not think about it, we can watch it for you.

Want it handled, not just checked?

Reeve Care keeps watching your app, backs up your data, and helps you fix what breaks, so you can keep building instead of worrying.

Learn about Reeve Care

FAQ

Is v0 code production-ready and secure?

v0 produces clean, modern code, but "production-ready looking" isn't the same as "checked." Security depends on how the app is wired: where keys live, whether your database rules are on, whether endpoints are protected. Reeve checks the exposed parts free in about 20 seconds.

Can a v0 app expose my API keys?

Yes, if a key ends up in a client component or a NEXT_PUBLIC_ setting. Next.js sends those to the browser. Not every key is a problem: some are meant to be public. Reeve finds the keys in your loaded code and tells you which are safe and which need to move to the server.

Are v0 API routes safe?

They can be, but a generated endpoint sometimes ships without an authentication check, which means anyone who finds it can call it. Reeve tests whether your endpoints answer to strangers; it only checks that the door opens, it never walks through it.

Will scanning my v0 app break anything?

No. Reeve only looks at what's already public from the outside. It never logs in, never changes anything, and never downloads data. Read-only, like checking whether a door is locked without going in.

What does the NEXT_PUBLIC_ prefix actually do?

It tells Next.js to compile that value into the JavaScript sent to the browser. The value stops being a server-side setting and becomes part of your published files, readable by any visitor. That is correct for a publishable key and wrong for a secret one, and the prefix is the only thing that decides it.

I renamed a variable to add NEXT_PUBLIC_ because the app could not read it. Was that a mistake?

It depends entirely on what the variable holds. If it was a publishable key or a project URL, the rename was the right fix. If it was a secret key, the app could not read it because it was never meant to run in the browser, and the rename published it. Rotate that key, then move the code that needed it to a server route.

Does v0 configure my database rules for me?

Not on its own. v0 generates interface code; the database and its rules are yours to set up wherever you host them. If you are on Supabase, Row Level Security is on by default only for tables created in the dashboard Table Editor, and off for tables created by running SQL.

Written by

Vlad Tkachenko

Founder, Reeve

I spend my time looking at apps built with Lovable, Bolt, v0, Cursor and Replit, and at the short list of mistakes that keep turning up in them.

More about the author

Built somewhere else? We've got the same honest rundown for:

← See all builder safety guides

Automated external check, not a full audit. Absence of findings is not a guarantee of safety.