Skip to content
BlackFig

v0 code messy: how to untangle a v0 project and keep it tidy

AntPublished 4 min read

Checked against v0's documentation on 9 November 2026.

Quick answer

v0 code gets messy when UI and data logic are generated in the same prompts, database queries are written inline without an ORM, and several chats change the same project on separate branches. Connect GitHub, move data access into one folder, split server and client components, add type checks and tests to CI, and save your structure as a v0 Instruction.

On this page
  1. Symptom
  2. Why it happens
  3. The fix
  4. How to prevent it
  5. When to get help

Symptom

None of this throws an error. The cost shows up as slower changes and more regressions each week.

Why it happens

UI and logic arrive in the same prompt

v0's full-stack docs recommend duplicating an existing generation to add logic, instead of combining UI and logic in a single prompt. Most people do the opposite: "add a bookings table with a form that saves to the database". The result is a component that renders, validates, queries and mutates in one file, often marked "use client" because part of it is interactive.

Queries without a data layer

The same docs say v0 connects to Supabase, Neon and Upstash without ORMs by default. That is a reasonable default for speed, but without a single module that owns queries, each feature writes its own. Column names, error handling and auth checks drift between copies.

Parallel chats, parallel code

With GitHub connected, each chat gets its own working branch and its own preview, and v0 opens a pull request when you publish. That is good practice. But two chats asked for overlapping features will both build them, and whichever merges second wins the conflicts. The Pull Changes action only appears when the base branch has commits the chat lacks, so a long-running chat can quietly work on stale code.

Fixes that make the build pass

v0 can fix missing files, dependencies, imports and runtime errors as part of its generation loop, and Fix with v0 reads deployment logs to repair builds. That is useful, and it optimises for a working build. It does not, by itself, merge duplicate helpers or move code to a better place.

The fix

  1. Get the code into GitHub and look at it properly. Connect from Project menu then Settings then GitHub, clone the repository and run the checks v0's preview does not show you:
pnpm install
pnpm exec tsc --noEmit                  # type errors the preview tolerated
pnpm exec next lint || pnpm exec eslint .  # depending on your Next.js version
git grep -nE "from\('|sql\`|\.query\(" -- "app/**" "components/**" | wc -l   # inline queries outside lib/

Note the numbers. They are your before measurement.

  1. Move data access into one folder. Create lib/db/ and make it the only place that talks to the database. With Neon's serverless driver that looks like this:
// lib/db/client.ts
import "server-only";
import { neon } from "@neondatabase/serverless"; // check against your driver version
 
export const sql = neon(process.env.DATABASE_URL!);
// lib/db/bookings.ts
import "server-only";
import { sql } from "./client";
 
export type Booking = { id: string; date: string; status: "pending" | "confirmed" };
 
export async function listBookingsForUser(userId: string): Promise<Booking[]> {
  const rows = await sql`
    select id, date::text, status
    from bookings
    where user_id = ${userId}
    order by date desc
  `;
  return rows as Booking[];
}

Then replace inline queries one feature at a time. Each replacement is a small pull request with a working preview.

  1. Split server and client components. Fetch in a Server Component, pass plain data to a small client component for the interactive part:
// app/bookings/page.tsx (Server Component)
import { requireUser } from "@/lib/auth/require-user";
import { listBookingsForUser } from "@/lib/db/bookings";
import { BookingList } from "./booking-list";
 
export default async function BookingsPage() {
  const user = await requireUser();
  const bookings = await listBookingsForUser(user.id);
  return <BookingList bookings={bookings} />;
}
// app/bookings/booking-list.tsx
"use client";
 
import { useState } from "react";
import type { Booking } from "@/lib/db/bookings";
 
export function BookingList({ bookings }: { bookings: Booking[] }) {
  const [filter, setFilter] = useState<Booking["status"] | "all">("all");
  const shown = filter === "all" ? bookings : bookings.filter((b) => b.status === filter);
  // ...render filter buttons and the list
  return <ul>{shown.map((b) => <li key={b.id}>{b.date}</li>)}</ul>;
}

A type-only import from a server-only module is erased at build time, so the client bundle does not pull in the database code.

  1. Let v0 write and run the tests. v0's terminal docs say it can run your project's test command in the sandbox, read the output and fix failures, and suggest prompts like "Run the unit tests and fix any failures." Ask for tests on the functions in lib/ first, where logic is pure enough to test. Its default permission mode is Auto; switch to Ask from the + menu next to the model picker if the sandbox has access to production data.

  2. Make CI the gate. Add a GitHub Actions workflow that runs tsc --noEmit, lint and tests on every pull request, and mark it as a required check. v0's docs say Publish respects branch protection, so a failing check stops a merge until you or v0 fix it, using Fix CI from the branch menu.

  3. Point v0 at exact code. In the Code tab, select lines and choose Add to chat to attach them, with a comment, to your next prompt. "Replace this inline query with listBookingsForUser" on a selected block is far more reliable than "clean up the bookings page".

How to prevent it

  • Save your structure as a v0 Instruction from the + menu under New Instruction: data access only in lib/db, Server Components fetch, client components receive props, no new helpers without searching for an existing one. Instructions are saved to your account and apply while checked.
  • Turn on the built-in Plan Mode instruction for anything touching more than two files. v0 then writes a plan and asks before changing code.
  • Keep one chat per stream of work, publish it, then start the next. Use Pull Changes before continuing an older chat.
  • Review the Diff tab on each message before you build on it.

If you are deciding whether to keep going or start again, my guide to refactoring or rebuilding a vibe-coded app has the questions I ask. Cursor projects get messy for different reasons, covered in Cursor codebase messy, and if you plan to move the code out of v0, start with exporting v0 code.

When to get help

A tangled codebase is fixable, but it is easy to spend weeks tidying the wrong parts. The free vibe code health check shows where the risk is. A Rescue Sprint audits the project and fixes the parts that block real users first. The guide to fixing vibe-coded apps covers the rest of the territory.

Sources

The vibe-code rescue checklist, as a PDF

About 40 checks across auth, data, payments, errors, mobile and secrets, on two pages.

You get the PDF now and two short follow-ups over the next week. Reply “stop” and the rest never arrive.

Questions founders ask

Is messy v0 code a reason to rebuild?

Usually not. Most v0 projects share one stack (Next.js, Tailwind, shadcn/ui), so moving data access and splitting components is a refactor, not a rewrite. Rebuild only when the data model itself is wrong.

Can v0 write tests for its own code?

Yes. v0's docs say it can run your test command in its sandbox, read the output and fix failures. You still need to decide what is worth testing.

Does v0 read an AGENTS.md file?

The v0 docs I read do not mention AGENTS.md. v0 documents Instructions, which are saved to your account and applied when checked, so put your rules there.

Ant

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