← All guides

v0 App Security Checklist: What to Check Before You Launch

Updated 2026-10-27

Short answer: a v0 app is a normal Next.js app, so it's as secure as the code it generated. Before launch, check five things: no secret keys in client code or NEXT_PUBLIC_ variables, every API route and server action checks who the user is, your database (for example Supabase) has row level security on every table, Stripe webhooks verify their signature, and anything that calls a paid AI API has a rate limit. Then test it like an attacker would, with curl and a second test account.

v0 is very good at producing a working UI and wiring it to a backend quickly. What it optimizes for is "it works when I click it". Security bugs don't show up when you click around as yourself, so they slip through. The checklist below is ordered by how often these problems show up in AI-built apps and how much damage they do.

1. Keep secrets on the server

In Next.js, any environment variable that starts with NEXT_PUBLIC_ is inlined into the JavaScript bundle and shipped to every visitor. That's fine for a Supabase URL or anon key, and wrong for anything that grants real power.

Check your Vercel project's environment variables and your code for these, and make sure none of them have the NEXT_PUBLIC_ prefix or appear in a file marked "use client":

A quick search catches most mistakes:

grep -rn "NEXT_PUBLIC_.*\(SECRET\|SERVICE_ROLE\|OPENAI\|STRIPE_SECRET\)" .
grep -rln "use client" app components | xargs grep -n "process.env" 

The second command lists client components that read environment variables. Anything other than a NEXT_PUBLIC_ value there is either broken or about to be "fixed" by adding the prefix, which leaks it. You can also open your deployed site, view the page source and the loaded JS files in dev tools, and search for sk_, service_role or eyJ.

If a secret was ever exposed, removing it from the code isn't enough. Rotate it. Our guide on rotating leaked Supabase and OpenAI keys walks through it, and exposed API keys in NEXT_PUBLIC variables explains the prefix rules in more detail.

2. Put an auth check in every API route

v0 often generates route handlers in app/api/*/route.ts that do exactly what the UI needs and nothing else. The UI only shows the "Delete" button to the owner, so the route assumes the caller is the owner. An attacker doesn't use your UI. They call the route directly.

Every route that reads or changes private data should start by getting the user on the server and checking ownership:

// app/api/projects/[id]/route.ts
import { createClient } from "@/lib/supabase/server";

export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return new Response("Unauthorized", { status: 401 });

  const { error, count } = await supabase
    .from("projects")
    .delete({ count: "exact" })
    .eq("id", id)
    .eq("owner_id", user.id); // ownership check, not just "logged in"

  if (error) return new Response("Error", { status: 500 });
  if (!count) return new Response("Not found", { status: 404 });
  return new Response(null, { status: 204 });
}

Two details matter. Use getUser() on the server, which validates the session with Supabase, rather than trusting a user ID sent in the request body. And check ownership, not just "is someone logged in". See how to secure Next.js API routes for patterns that cover roles and admin routes.

3. Treat server actions like public endpoints

Server actions (functions marked "use server") feel like internal function calls, but Next.js exposes each one as an HTTP endpoint that anyone can call with any arguments. If v0 generated an action like updateProfile(userId, data), a caller can pass someone else's userId.

Inside every server action, get the user from the session, validate inputs (for example with zod), and never accept an ID that decides whose data gets changed. Our Next.js server actions security guide has full examples.

Also check middleware. If your app relies only on middleware.ts to protect pages and routes, make sure you're on a patched Next.js version and still check auth inside the route itself. The middleware bypass CVE-2025-29927 guide explains why.

4. Turn on RLS for every Supabase table

Most v0 apps that store data use Supabase. The anon key is public by design, which means the only thing between a stranger and your tables is row level security. A table without RLS can be read, and often written, by anyone who copies the anon key out of your site.

Run this in the Supabase SQL editor to find tables without RLS:

select schemaname, tablename
from pg_tables
where schemaname = 'public' and rowsecurity = false;

Then enable it and add policies that tie rows to auth.uid():

alter table public.projects enable row level security;

create policy "owners read their projects"
on public.projects for select
using (owner_id = auth.uid());

create policy "owners change their projects"
on public.projects for all
using (owner_id = auth.uid())
with check (owner_id = auth.uid());

Watch for policies like using (true) on tables with private data, which make RLS look enabled while allowing everything. The Supabase RLS checklist covers the common mistakes, and if you store uploads, also check Supabase Storage bucket security.

5. Verify Stripe webhooks and set prices on the server

If your v0 app takes payments, check two things:

6. Rate limit anything that costs money

v0 makes it easy to add AI features with the AI SDK. If a route calls OpenAI or another provider, anyone who finds it can call it in a loop and run up your bill. At minimum, require a logged-in user and add a per-user or per-IP limit. Rate limiting OpenAI API routes shows a working setup with Upstash and a simpler fallback. Also set a monthly spend limit in your AI provider's dashboard as a backstop.

The same applies to signup, password reset, contact forms and anything that sends email.

7. Check what's public on Vercel

A few deployment settings are easy to miss:

8. Test it like an attacker

Before you share the link, spend 20 minutes on these tests:

  1. Open the deployed site, view source and search the JS bundles for sk_, service_role and your AI provider's key prefix.
  2. Call your Supabase REST API directly with the anon key and confirm you can't read other users' rows:
curl "https://YOUR-PROJECT.supabase.co/rest/v1/projects?select=*" \
  -H "apikey: YOUR_ANON_KEY"
  1. Create two test accounts. Logged in as user A, copy a request from the network tab that touches user B's data (change the ID) and replay it. It should fail.
  2. Call each API route with curl and no cookies. Private routes should return 401.
  3. Send a fake event to your Stripe webhook URL with curl. It should be rejected with a 400.

If you're also using Cursor to edit the code v0 produced, the Cursor generated code security review has more checks, and the pre-launch security checklist for AI-built apps covers monitoring and backups.

Get an automated first pass

PreflightPR checks AI-built Next.js apps for leaked keys, API routes without auth checks, Supabase tables without RLS, unverified Stripe webhooks and AI endpoints without rate limits. Push your v0 project to GitHub, then run a free scan on the public repo to get a launch score and a prioritized fix list, or install the PreflightPR GitHub App to check every pull request before it merges. It's an automated best-effort review, not a pen test, so keep the manual tests above too.

Check your repo in about a minute

Paste a public GitHub URL and get a free launch score.

Run a free scan

More guides