The complete guide to fixing a vibe-coded app
Fix a vibe-coded app in order of risk. Stop prompting, then check exposed keys and access rules, then the data model and payments, then error states, speed, mobile and SEO. Most apps need targeted fixes rather than a rewrite, but get the code into GitHub and out of the fix loop first.
On this page
- In this guide
- The order to fix things in
- Security and exposed keys
- Auth and access rules
- Database and data model
- Payments that drift from Stripe
- Error handling
- Slow apps
- Mobile layout
- SEO that never shows up
- Messy codebases
- Refactor, rescue or rebuild
- Handing the code over
- Every fix by tool and problem
- How a Rescue Sprint works
In this guide
This is the hub for everything that goes wrong after the prototype works. It covers apps built with Lovable, Bolt, v0, Cursor and Replit, and every section ends with links to the detailed, step-by-step fixes.
- The order to fix things in, and why prompting harder rarely works
- Security and exposed keys: secret keys in the browser and how to check for them
- Auth and access rules: row level security, redirect URLs and email limits
- Database and data model: constraints, indexes, backups and separate environments
- Payments that drift from Stripe: why the success page is the wrong place to upgrade someone
- Error handling: the states AI tools never build
- Slow apps: what to measure and the usual causes
- Mobile layout: fixed widths, tap targets and contrast
- SEO that never shows up: rendering, titles and soft 404s
- Messy codebases: how each tool shapes the mess, and how to stop it growing
- Refactor, rescue or rebuild: how to decide
- Handing the code over to a developer, including me
- Every fix by tool and problem: one table linking every tool-specific guide
- How a Rescue Sprint works
If you want the short version first, my page on fixing a vibe-coded app has ten tests you can run yourself in an afternoon. This guide goes further: what each failure looks like in each tool, what the fix actually involves, and where to read the full version.
The order to fix things in
Every AI builder is good at the same thing: the screen in front of you. You describe it, it appears, and it works when you click it. I use these tools myself, and they are the fastest way I know to get an idea in front of people.
What they don't do is hold the whole system in mind. Each prompt is a local change. Nobody prompts "make sure user B can't read user A's invoices", so it never gets built, and the app looks finished because everything you can see is finished. I go into the underlying causes in why AI-built apps break.
The most expensive habit at this stage is the fix loop. You describe the bug, the tool patches it, something else breaks, and you describe that. Each patch adds code around a symptom. After a few rounds the app is bigger, slower and harder to change, and the original cause is still there.
So the first step is to stop prompting and look. Then fix in order of risk:
Stop the fix loop
Pause new prompts on the broken area. Connect the project to GitHub if it isn't already, so every change from here on is recorded and can be reversed.
Keys and access rules
Exposed secret keys, missing row level security and anything that lets one user see another user's data. A leak can't be undone, so this always comes first.
The data model
Duplicated tables, missing constraints and missing indexes. Fixing these early stops every later change from getting harder.
Money
Payments, subscriptions, refunds and cancellations, so your records match what Stripe says people actually paid.
The experience
Error states, speed, mobile layout, accessibility and SEO. This is where the app starts to feel like a real product.
For a structured first pass, work through the vibe-code rescue checklist, or run the free health check for a quick read on where your app stands before anyone touches the code.
Security and exposed keys
Most AI-built apps talk to their database straight from the browser. With Supabase, a common backend for AI-built apps and one Lovable connects to directly, that is a normal, supported pattern. It is only safe if two things are true: the browser holds the right kind of key, and the database decides what that key can reach.
Supabase has two kinds of key. Publishable keys (the legacy version is called anon) are designed to be public. Supabase's docs say anyone can read one, so it only reaches what Row Level Security allows. Secret keys (the legacy version is service_role) bypass Row Level Security completely and must never appear in a browser, a shipped app or source control. Supabase is deprecating the legacy anon and service_role keys by the end of 2026, so if your app still uses them, make the switch part of the fix.
The failure I look for first is a secret key in front-end code. It tends to happen when a prompt hits a permissions error and the quickest way to make the error go away is the key that ignores permissions. Supabase now rejects secret keys sent from a browser, but its own docs are clear that this is not a safeguard: anyone who copies the key can still use it from other tools. If a secret key has ever been in front-end code, rotate it.
Next.js projects, which includes everything v0 builds and many Cursor projects, have a second trap. Vercel's docs explain that variables starting with NEXT_PUBLIC_ are inlined into the client bundle at build time. A variable called NEXT_PUBLIC_STRIPE_SECRET_KEY is not secret, whatever its name says. Stripe's docs say only publishable keys (starting pk_) are safe to expose. Secret keys (sk_) and restricted keys (rk_) belong on the server.
You can check a built app yourself. Build it, then search the files that get sent to the browser:
# Next.js (v0 and many Cursor projects): the client bundle lives in .next/static
npm run build
grep -rlE "sk_live_|sk_test_|rk_live_|sb_secret_" .next/static
# Vite projects (older Lovable projects, for example): the client build goes to dist/
grep -rlE "sk_live_|sk_test_|rk_live_|sb_secret_" dist
# Legacy Supabase service_role keys are JWTs, so their prefix looks like the anon key.
# Search for the last 20 characters of your actual service_role key instead.
grep -rl "LAST_20_CHARACTERS_OF_YOUR_KEY" .next/static distEach tool gives you some help here, and none of it is the whole job:
- Lovable runs a Quick scan every time you publish, covering database access rules, known vulnerabilities in npm packages and MCP servers exposed without sign-in. The Deep scan, which reviews your code for leaked secrets, payments, auth and access control, only runs when you ask for it. Lovable says plainly that its tools cannot guarantee complete security.
- Bolt has a Security Audit that checks the database for open permissions and missing row level security policies. It does not scan the rest of your project.
- Replit keeps keys in its Secrets tool, and production secrets are set separately in the publishing settings. Its docs warn that hard-coded secrets get exposed when code is shared through a public project or copied and pasted.
For the full walk-through, read the security risks in vibe-coded apps and how to find and rotate exposed API keys in AI code.
Auth and access rules
Login usually works. What breaks is authorisation: deciding what each signed-in person is allowed to see and change. They sound alike, but the first is a form and the second is a rule on every table.
In Supabase-backed apps, that rule is a Row Level Security policy. Supabase's docs say that once RLS is enabled, no data is reachable through the API with a publishable key until you create policies. That explains a pattern I see a lot: the app shows a blank screen after RLS is switched on, the founder prompts "fix the empty dashboard", and the tool writes a policy that lets any signed-in user read every row. The dashboard works again. The leak is invisible until someone looks.
Fixed looks like one policy per action per table, tied to the row's owner:
-- Each user can read and change only their own projects
alter table public.projects enable row level security;
create policy "Owners can read their projects"
on public.projects for select
to authenticated
using ( (select auth.uid()) = owner_id );
create policy "Owners can update their projects"
on public.projects for update
to authenticated
using ( (select auth.uid()) = owner_id )
with check ( (select auth.uid()) = owner_id );
-- The policies filter on owner_id, so index it
create index if not exists projects_owner_id_idx on public.projects (owner_id);Two details there come straight from Supabase's guidance. Wrapping auth.uid() in a select lets Postgres work it out once per query rather than once per row. And every column a policy filters on should be indexed, because an unindexed filter turns each read into a full table scan.
Then test it the way an attacker would. Sign in as user A, copy the URL of one of their records, open a private window, sign in as user B and paste it. Look for admin pages that are hidden in the menu but not blocked in the database, and for roles stored in a profile table the user can edit.
The sign-in flows themselves fail in quieter ways:
- Reset and confirmation links that open the wrong site. Supabase uses the Site URL as the default redirect, and any
redirectToin your code has to match the Redirect URLs list. If a link opens a preview address or localhost, this is usually why. Supabase supports wildcards for preview URLs but recommends exact paths in production. - Confirmation emails that stop arriving. On a Supabase project you manage yourself, the built-in email sender is limited to 2 emails an hour until you set up your own SMTP provider. On launch day, that means most new users never get their confirmation email.
- Login that was never added. Bolt's help centre notes that features like authentication may not be added automatically. If login arrived late, records created before it may have no owner at all.
The general fix is in fixing auth in an AI-built app. For the tool-specific causes, read Lovable auth not working, Bolt auth not working, v0 auth not working, Cursor auth not working and Replit auth not working.
Database and data model
AI tools build the database one prompt at a time, so the schema records the order you thought of things, not how the business works. The usual results: the same thing stored in two tables, a customer's name saved as text instead of a link to the customer, no rules against duplicates, and prices stored as decimals that round badly. It all works with ten rows of test data. With real users, reports stop adding up and records go missing.
Fixed looks like a schema you can draw on one page, with the rules enforced by the database rather than only by a form:
-- Link bookings to customers properly instead of storing the name as text
alter table public.bookings
add column customer_id uuid references public.customers (id) on delete restrict;
-- Backfill customer_id from the old text column before you drop it
-- One booking per customer per slot, enforced by the database
alter table public.bookings
add constraint bookings_customer_slot_unique unique (customer_id, slot_id);
-- Store money as whole pence, never as a floating point number
alter table public.bookings
add column amount_pence integer not null default 0 check (amount_pence >= 0);Where the database lives depends on the tool, and that changes what a fix involves:
- Lovable uses either its built-in backend or a Supabase project you connected yourself.
- Bolt creates a database in Bolt Cloud when your project needs one. Its docs say there is currently no supported way to migrate an existing Bolt database to Supabase, so that decision matters early.
- Replit gives every published app a production database separate from the development one. Both use a variable called
DATABASE_URLwith different values. Replit's Agent can't change the production database directly, but structural changes you make in development are applied to production when you publish. Treat each publish as a database migration.
Two more things to check before real data goes in. First, backups. On Supabase, Pro projects get daily backups kept for 7 days, and Free plan backups can't be downloaded, so Supabase suggests exporting regularly with the CLI. Files in Storage are not included in database backups at all. Second, pausing. Supabase pauses Free plan projects after low activity over 7 days. Paid projects are never paused. A staging project you forgot about will be.
The full process is in fixing the database schema in an AI-built app. For tool-specific problems, read Lovable and Supabase issues and Bolt database issues.
Payments that drift from Stripe
The most common payment bug in AI-built apps is simple: the app upgrades the user when the success page loads. If the tab closes first, or the connection drops, you take the money and the account stays on the free plan.
Stripe's docs are direct about this. You can't rely on fulfilment from the landing page, because customers aren't guaranteed to reach it, and webhooks are required if you sell subscriptions. Stripe also says events can arrive more than once and in any order, so a handler has to verify the signature, record which events it has processed, and not assume "created" always arrives before "paid".
Refunds, failed renewals and cancellations often have no code path at all. The result is a database that slowly disagrees with Stripe, and nobody notices until a customer emails.
Payments sit in my launch guide because they are part of going live, not only a bug. Read adding Stripe to an AI-built app for the full setup, and the payments section of from prototype to product for how it fits into a launch.
Error handling
AI tools build the state where everything worked. They rarely build the empty list, the slow network, the expired session or the failed save. Users then get a blank screen, a spinner that never stops, or worse, a message saying their change was saved when it wasn't.
Here is a pattern I find in almost every Supabase-backed app. The Supabase client returns an error object instead of throwing, and generated code often ignores it:
// Before: the error is ignored, so a failed save looks like a successful one
const { data } = await supabase.from("projects").insert({ name });
// After: check the error, tell the user, and log something you can find later
const { data, error } = await supabase
.from("projects")
.insert({ name })
.select()
.single();
if (error) {
console.error("Project insert failed", { code: error.code, message: error.message });
toast.error("Couldn't save your project. Check your connection and try again."); // toast from sonner, as in most shadcn/ui projects
return;
}Fixed looks like four states on every screen that loads data: loading, empty, error and success. It also means forms that keep what the user typed when a submit fails, sessions that expire with a clear prompt to sign in again, and an error log you actually read.
A quick test: switch off wifi and use the app, then submit every form empty. If anything goes blank or claims success, it needs work. The full guide is error handling in AI-generated code, and the design side is covered in empty, error and loading states.
Slow apps
"It feels slow" is hard to fix. A number is easier. Google's Core Web Vitals give you three, measured at the 75th percentile of real page loads: the main content should appear within 2.5 seconds (Largest Contentful Paint), the page should respond to taps and clicks within 200 milliseconds (Interaction to Next Paint), and the layout shouldn't jump about by more than 0.1 (Cumulative Layout Shift).
The causes in AI-built apps are remarkably consistent:
- Everything renders in the browser. The user downloads a large JavaScript bundle, waits for it to run, and only then does the app start fetching data. Older Lovable projects are React and Vite single-page apps that work exactly this way. Lovable projects created from 13 May 2026 render on the server with TanStack Start instead.
- Request chains. The page fetches the user, then their organisation, then their projects, then each project's details, one after another.
- Unindexed queries. Fine on test data, painful at a few thousand rows, and worse when a row level security policy filters on the same unindexed column.
- Full-size images. A 4,000 pixel photo shown in a 300 pixel card.
Fix them in that order of evidence: measure, find the slowest step, fix it, measure again. Indexes and parallel requests are cheap to try. Server rendering is a bigger change and worth it for pages that need to be found in search.
Read why an AI-built app is slow for the method, then Lovable app slow or Bolt app slow for the tool-specific causes.
Mobile layout
Most vibe-coded apps were built in a desktop browser preview, and it shows. Tables run off the side of the screen, modals are taller than the phone, menus only open on hover, and buttons are too small to hit with a thumb.
Two WCAG criteria give you clear targets here. WCAG 2.2 success criterion 2.5.8 (level AA) says tap targets should be at least 24 by 24 CSS pixels, with some exceptions. Criterion 1.4.3 (also AA) says normal text needs a contrast ratio of at least 4.5:1 against its background, or 3:1 for large text. The pale grey text AI tools love often fails the second one.
The fix is rarely a full redesign. It is usually a handful of components made responsive properly. Tables are the classic example:
// Show a table from the md breakpoint up, and stacked cards on phones
<div className="hidden md:block">
<BookingsTable rows={rows} />
</div>
<ul className="space-y-3 md:hidden">
{rows.map((row) => (
<li key={row.id} className="rounded-lg border p-4">
<p className="font-medium">{row.customerName}</p>
<p className="text-sm text-muted-foreground">{row.date}</p>
</li>
))}
</ul>Test the whole journey on your own phone, one-handed, over mobile data: sign-up, the main task and payment. Then read fixing the mobile layout of an AI app and the tool guides for Lovable mobile layout, Bolt mobile layout and v0 mobile layout. For the wider accessibility pass, see making an AI-built app accessible.
SEO that never shows up
If your app has public pages you want people to find, such as a landing page, listings or profiles, check whether Google can actually see them.
Google's own documentation explains that it processes JavaScript apps in three phases: crawl, render, index. Pages wait in a queue for rendering, which can take a few seconds or longer. Google still recommends server-side rendering or pre-rendering because it is faster for users and for crawlers, and not every bot runs JavaScript. Its guidance for single-page apps also asks for unique titles and descriptions per page, routing with the History API rather than URL fragments, and a real 404 status (or a noindex tag) for pages that don't exist, so they aren't treated as soft 404s.
What I find in AI-built apps:
- The same title and description on every page.
- Public content that only exists after JavaScript runs.
- Missing pages that return a normal page with "Not found" in it, rather than a 404.
- Preview domains and private dashboards open to crawlers.
The tools are moving on this. Lovable projects created from 13 May 2026 are server-rendered, and Lovable says existing apps now get a server-rendered version when a verified crawler visits. Replit shows an SEO rating for published pages, and paid plans can use its SEO Agent to suggest improvements. None of that writes your titles or fixes your 404s for you.
Read the tool guides for Lovable SEO not working, Bolt SEO not working and Replit SEO not working.
Messy codebases
A messy AI codebase has a recognisable look: three versions of the same button, files over a thousand lines long, dead code from features you dropped, and two different ways of fetching data side by side. None of it is visible to users. All of it makes the next change slower and riskier.
Each tool shapes the mess differently:
- Cursor works inside your own repository, so the mess is whatever patterns the agent picked up. Cursor's project rules live in
.cursor/rulesas.mdcfiles withdescription,globsandalwaysApplysettings, and it also reads anAGENTS.mdfile. Its docs suggest keeping rules under 500 lines, splitting large ones up, and adding a rule only when the agent repeats the same mistake. - v0 gives every chat its own working branch and preview. Publishing merges that branch through a pull request. Long-running chats drift from the base branch, and that is where duplicate components come from.
- Lovable and Bolt both sync two ways with GitHub. That is good for a clean-up, because a developer can fix code in the repository and you keep prompting. It also means your next prompt can quietly undo the clean-up.
- Replit builds full-stack apps with its Agent in one workspace, and the boundary between front end, server and database code is often the first thing to blur.
A short rules file stops the most common regressions. For Cursor, it might look like this:
---
description: Data access and money rules for this project
alwaysApply: true
---
- All database reads and writes go through functions in src/lib/data. Never call the database client from a component.
- Never use a secret key outside src/lib/server.
- Money is stored as integer pence. Never use floating point numbers for prices.
- Reuse components from src/components/ui before creating a new one.Then clean up in small, tested steps rather than one big refactor. Read the tool guides on a messy v0 codebase, a messy Cursor codebase and a messy Replit codebase.
Refactor, rescue or rebuild
Once you know what's wrong, the real decision is how much to change. There are three sensible answers.
| Refactor in place | Rescue Sprint | Rebuild | |
|---|---|---|---|
| Best when | A few problems, all in known areas | Several serious problems, but the product is right | The data model or the product itself is wrong |
| Data model | Sound | Fixable with migrations | Wrong at the root |
| What you keep | Almost everything | Everything that works | The design, the copy and the lessons |
| Keep using the AI tool | Yes | Yes, with notes on what to leave alone | Optional |
| Typical trigger | One or two tests fail | Security, data and payment tests fail | Seven or more of the ten basic tests fail |
My rule of thumb: if the problems are in how it was built, rescue it. If the problems are in what was built, rebuild it. A two-sided marketplace built as if there were only one kind of user is a "what" problem, and every screen depends on it.
A rebuild doesn't waste the prototype. It is the clearest brief you could write. An MVP Build starts from £12,000 and takes 4 to 6 weeks. For the full reasoning, read refactor or rebuild a vibe-coded app. For budgets, see what it costs to fix an AI-built app and what an MVP costs in the UK.
Handing the code over
Whether the code is going to me, a freelancer or your first hire, the handover goes faster when you prepare a few things first. Most of the delay in starting a fix is access, not code.
Before you hand over an AI-built app
- The code in a GitHub repository you own, with the developer invited as a collaborator.
- A list of every environment variable name and where its value lives. Send values through a password manager, never in chat or email.
- Access to the database project as a team member, not a shared login.
- Access to Stripe as a team member, so nobody needs your password.
- Access to the domain registrar and DNS, or the name of whoever has it.
- A short list of what the app must do, who uses it, and what is broken right now.
- Any prompts or notes that explain why something was built the way it was.
Getting the code into GitHub works differently in each tool, and the details matter:
- Lovable always creates a new repository when you connect. It syncs one branch at a time and pushes to a branch called
lovable-syncif a push clashes with your branch rules. - Bolt only lets the project owner connect GitHub. Disconnecting removes the link permanently from your side, and reconnecting the same repository means contacting support.
- v0 creates a private repository that becomes the source of truth for the code. Its docs warn that deleting the repository can make the code unrecoverable.
- Replit connects to GitHub through its Git pane, with auto-sync for two-way changes, or you can download a zip. Copying a project doesn't bring its secrets, database or deployment settings with it.
Read handing over AI-generated code for the full process, and exporting AI-built code for each tool's export steps.
Every fix by tool and problem
This table links every tool-specific fix guide. Where a tool doesn't have its own page, the general guide covers it.
Payments, deployment and code export are part of going live, so they sit in from prototype to product. For Lovable and Bolt specifically, my guides to taking a Lovable app to production and taking a Bolt app to production cover hosting, export and limits in one place.
How a Rescue Sprint works
If you would rather hand the whole list to someone, this is how I do it. A Rescue Sprint is an audit plus fixes for an AI-built app, at a fixed price agreed after the free health check. It usually takes about a week.
Free health check
Run the vibe-code health check. It gives you, and me, a first read on where the app stands.
Call and read access
A short call about who uses the app and what it must do, then read access to the repository so I can check the areas in this guide properly.
Fixed price, in writing
A short list of what I will fix, in what order, and one price. If something is better rebuilt than rescued, I say so at this point.
Keys, access rules, data and payments
The dangerous things first: rotated keys, row level security on every table, a sound schema, and payments driven by webhooks.
Errors, speed, mobile and SEO
Real error and empty states, the slowest pages fixed, a phone-first pass and the basics search engines need.
Tests and handover
Tests around sign-up, payment and the core journey, plus notes on which files to leave alone when you go back to prompting.
I don't quote a number before the health check and a call, because two apps that look the same on the surface can be very different underneath. You can keep using Lovable, Bolt, v0, Cursor or Replit afterwards. Most founders do.
See what the Rescue Sprint includes, or run the free health check first if you want to see where your app stands.
- Supabase docs: API keys, checked 9 November 2026
- Supabase docs: Row Level Security, checked 9 November 2026
- Supabase docs: Redirect URLs, checked 9 November 2026
- Supabase docs: Production checklist, checked 9 November 2026
- Supabase docs: Database backups, checked 9 November 2026
- Supabase docs: Project pausing, checked 9 November 2026
- Vercel docs: Environment variables, checked 9 November 2026
- Stripe docs: API keys, checked 9 November 2026
- Stripe docs: Receive Stripe events in your webhook endpoint, checked 9 November 2026
- Stripe docs: Fulfill orders (Checkout), checked 9 November 2026
- Lovable docs: Security, checked 9 November 2026
- Lovable docs: GitHub integration, checked 9 November 2026
- Lovable blog: Building apps using TanStack Start, checked 9 November 2026
- Bolt help: Database, checked 9 November 2026
- Bolt help: GitHub integration, checked 9 November 2026
- v0 docs: GitHub, checked 9 November 2026
- Cursor docs: Rules, checked 9 November 2026
- Replit docs: Secrets, checked 9 November 2026
- Replit docs: Development and production databases, checked 9 November 2026
- Replit docs: Publish your app, checked 9 November 2026
- Replit docs: Projects and files, checked 9 November 2026
- web.dev: Web Vitals, checked 9 November 2026
- Google Search Central: JavaScript SEO basics, checked 9 November 2026
- W3C: Understanding WCAG 2.2, Target Size (Minimum), checked 9 November 2026
- W3C: Understanding WCAG 2.2, Contrast (Minimum), checked 9 November 2026
The vibe-code rescue checklist, as a PDF
About 40 checks across auth, data, payments, errors, mobile and secrets, on two pages.
Questions founders ask
Can I fix a vibe-coded app by prompting the AI tool again?
For small, visible bugs, often yes. For access rules, data models and payments, repeated prompting tends to add code around the cause rather than remove it. Find the cause first, then decide whether a prompt or a hand fix is the safer change.
What should I fix first in an AI-built app?
Anything that can leak data or lose money: exposed secret keys, missing or loose row level security, and payment handling that relies on a success page. Then the data model, then the experience: errors, speed, mobile and SEO.
Do I need to rebuild my vibe-coded app from scratch?
Usually not. A rebuild makes sense when the data model is wrong at the root, the product has changed direction, or most of the basic checks fail. Otherwise targeted fixes keep what works and cost less.
Can I keep using Lovable or Bolt after the app is fixed?
Yes. Lovable, Bolt, v0 and Replit can all sync code with GitHub, so fixes made in the repository flow back into the tool. The risk is a later prompt quietly undoing a fix, so keep notes on which files to leave alone.

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