How to Fix a Vibe Coded App (Without Making It Worse)
Your app worked last week. Now it is down, or a stranger on the internet just told you your users' data is readable, or the AI that built it has been "fixing" the same bug for two days and the codebase is drifting somewhere you cannot follow. Take a breath. You did not do anything unusually careless. You did what the tools are built for: you shipped fast. The cleanup is a known process, and you can run it. The failure modes of AI-built apps are boringly predictable, which is good news, because predictable means fixable. Escape.tech scanned more than 5,600 vibe coded apps and found over 2,000 vulnerabilities reachable through public endpoints, plus 400+ secrets sitting in frontend bundles. Veracode tested output from more than 100 LLMs and found 45 percent of AI-generated code samples introduced OWASP Top 10 class vulnerabilities, and newer models were not more secure than older ones. If your app has one of these problems, you are not an outlier. You are the median. This is a rescue runbook, not a lecture. Do the steps in order, because the order is the point: stop active data loss first, take a snapshot second, and only then start fixing things. Most rescues do not end in a rebuild. They end with a working app and a short list of habits that keep it working.
Step 1: Step 1. Triage: is it down, leaking, or just broken?
Every broken app falls into one of three buckets, and the bucket decides what you do next. Down means nobody can use it. Leaking means data or secrets are exposed to people who should not have them. Broken means a feature fails but the app is up and not exposing anything. The danger order is: leaking beats down, and down beats broken. A leak gets worse every hour you spend polishing a button.
Check for leaking first, because it is the failure that shows no error message. Open your deployed site, view the page source or the DevTools Network tab, and look for anything that resembles a secret key in the JavaScript (see /errors/vibe-coding/api-key-in-frontend/ for what that looks like). Open your Supabase dashboard and look for 'RLS disabled' warnings, the pattern behind /errors/supabase/rls-off/. Check the billing pages of every paid API you use (OpenAI, Stripe, Anthropic) for usage you do not recognize. Search your inbox for secret scanning alerts from GitHub or GitGuardian. Any hit here sends you straight to Step 2.
If nothing is leaking, sort down from broken. Load the app in an incognito window and write down exactly what fails. Open the browser console (right-click, Inspect, Console) and copy the first red error; that message is the actual cause, not the vague toast the app shows. Two patterns worth knowing: if everything died at once and your backend is a free Supabase project, it was probably paused for inactivity (/errors/supabase/project-paused-app-dead/ has the restore steps, and the fix takes minutes). If the page is a blank white screen, a JavaScript error is crashing the app before it can render (/errors/lovable/blank-white-screen/ walks through it).
- Load the app in an incognito window and write the failure down in one sentence
- Open the browser console and copy the first red error, word for word
- View the page source and search for anything that looks like a secret key
- Open the Supabase dashboard and check for RLS warnings or a Paused project
- Check every paid API's usage dashboard for activity you do not recognize
- Search your inbox for secret scanning alerts from GitHub or GitGuardian
Step 2: Step 2. Stop the bleeding: rotate keys, enable RLS, pull the plug if you must
If triage found anything leaking, fix that before you fix anything else, including the outage. Start with keys. Search your shipped code and your repo for the telltale prefixes: sk-, sk_live_, service_role, and eyJ (the start of a JWT). Any secret that ever appeared in a frontend bundle, a public repo, or an AI chat window is compromised, full stop. Removing it from the code does not un-leak it; copies live on in git history, chat history, and whatever scrapers already grabbed. Rotate each one at the provider's dashboard right now. The details differ by platform: /errors/vibe-coding/api-key-in-frontend/ covers the general case, /errors/lovable/api-keys-exposed/ covers Lovable specifically, and /errors/vibe-coding/secrets-committed-to-github/ covers keys sitting in your git history.
Next, Row Level Security. If your app uses Supabase and the AI created your tables, assume RLS is off until you prove otherwise. This is the exact pattern behind CVE-2025-48757: a scan of 1,645 public Lovable projects found about 10 percent with inadequate RLS, meaning anyone with the anon key from the JavaScript bundle could read every row of every table. In the Supabase dashboard, open Database > Advisors and run the Security Advisor; it flags every public-schema table with RLS disabled. For each one, run: ALTER TABLE public.your_table ENABLE ROW LEVEL SECURITY; Know what happens next: enabling RLS with zero policies blocks all anonymous access, so parts of your app will break. That is the safe direction to break in, and you write proper policies in Step 5. Full walkthroughs live at /errors/supabase/rls-off/ and /errors/lovable/rls-disabled-data-exposed/. One more thing: assume any data that sat in an RLS-less table was already read, and rotate any secrets that were stored in it.
If you cannot stop the leak within minutes (you cannot find the key, or the exposure is structural), take the app offline. Pause the deployment or put it behind a password. A deliberately dark app costs you a day of users; a leaking app costs your users their emails, addresses, and stored credentials. While you are in the billing dashboards anyway, set hard spend caps and alerts on every paid API. Do not assume a 'usage limit' field is a cap; OpenAI's has historically behaved as a notification threshold rather than a hard stop, which is how people wake up to four-figure bills (/errors/vibe-coding/no-rate-limiting-surprise-bill/).
- Search code, bundle, and git history for sk-, sk_live_, service_role, and eyJ
- Rotate every exposed key at the provider dashboard; do not just delete it from code
- Run Supabase Security Advisor (Database > Advisors) and list every table with RLS off
- Enable RLS on each exposed table, accepting that the app breaks in the safe direction
- Rotate any secrets that were stored inside an exposed table
- Take the app offline if you cannot contain the leak within minutes
- Set hard spend caps and billing alerts on every paid API
Step 3: Step 3. Back up everything, including the broken parts
Before you or any AI agent changes another line, take a snapshot of the whole mess exactly as it is. Yes, even the broken state. The rescue process involves a lot of edits in a short time, some of them by an AI, and you want a way back to today. Commit everything and push it to GitHub (connect GitHub sync first if your platform has it and you have not). An agent cannot destroy what is committed and pushed.
The database needs its own backup, because code snapshots do not contain data. On a Supabase Pro plan, confirm daily backups exist under Database > Backups (or PITR if you enabled it). On the free plan there are no downloadable automated backups, so make your own: run pg_dump against the project's connection string, or at minimum use the dashboard's CSV export for your most important tables. This step feels skippable. It is the least skippable step in the runbook: /errors/vibe-coding/no-backup-before-ai-refactor/ documents what happens to people who skip it.
Two hard-won rules while you are here. First, never take an AI agent's word for what is recoverable. In the July 2025 Replit incident, the agent deleted a production database with records for over 1,200 executives, then told the founder rollback was impossible. It was not; the data was recoverable from a platform backup the agent claimed did not exist (/errors/replit/agent-deleted-database/). Check the platform's own backup tooling yourself. Second, version history in tools like Lovable restores code only. Reverting the project does not bring back deleted database rows (/errors/supabase/migration-wiped-database/), so do not treat the revert button as a database backup.
- Commit the current state, broken parts included, and push to GitHub
- Confirm database backups exist (Supabase Pro: Database > Backups), or run pg_dump yourself
- On the Supabase free plan, export critical tables to CSV at minimum
- Write down which backup you took and when, so you know your restore point
- Verify recoverability in the platform's own tooling, never from an agent's claims
Step 4: Step 4. Audit what the AI actually built
Now read what you own. Sync the project to GitHub if you have not and open it in an editor. You are not becoming a software engineer this afternoon; you are pattern-matching against a short list of known AI failure modes, and every one of them is greppable. This audit is a standard step for AI-built apps, not a punishment: Veracode found models failed to defend against cross-site scripting in 86 percent of relevant tasks and log injection in 88 percent, and asking the model for 'secure code' measurably is not enough.
Search for these patterns. String-built SQL: template literals or f-strings containing SELECT, INSERT, UPDATE, or DELETE with variables interpolated in; every hit is an injection risk (/errors/vibe-coding/injection-flaws/). Raw HTML rendering: dangerouslySetInnerHTML, innerHTML, or v-html with user input. Wildcard CORS: app.use(cors()) with no options, or allow_origins set to a wildcard, which lets any website call your API from a logged-in user's browser (/errors/vibe-coding/wildcard-cors/). Misplaced secrets: anything sensitive behind a VITE_ or NEXT_PUBLIC_ prefix is compiled into the public bundle by design. And unauthenticated routes: list every API route your app exposes and label each one public or authenticated. If you cannot justify 'public', it is authenticated. AI tools scaffold working endpoints, not protected ones, and hiding a button in the UI protects nothing (/errors/vibe-coding/no-auth-on-endpoints/).
Audit the dependencies too. LLMs hallucinate plausible package names at scale (a 2.23 million sample study found 19.7 percent of generated samples referenced at least one nonexistent package), and attackers register the recurring fakes on npm and PyPI. Look up any package you do not recognize on npmjs.com or pypi.org: check weekly downloads, publish date, and the linked repo. A brand-new package with a generic name matching an AI suggestion is the red flag, and an install that 404s is a hallucination signal, not a typo (/errors/vibe-coding/slopsquatting/). Finish the audit by running a scanner over the whole repo: Semgrep or Snyk Code for the code, gitleaks for secrets hiding in history. Scanners catch these patterns faster than tired human eyes do.
- Sync the project to GitHub and open the code in a real editor
- Grep for string-built SQL and convert hits to your fix list
- Grep for dangerouslySetInnerHTML, innerHTML, and v-html with user input
- Grep for cors() with no options and wildcard allowed origins
- Check every VITE_ and NEXT_PUBLIC_ variable: those values are public
- List every API route and label it public or authenticated
- Verify every unfamiliar dependency on npmjs.com or pypi.org before trusting it
- Run Semgrep or Snyk Code on the repo and gitleaks on the history
Step 5: Step 5. Fix in order of severity, not in order of annoyance
The visible bug is rarely the dangerous one. Work your fix list in this order: security-critical, then data risk, then blockers, then cosmetics. It is tempting to fix the thing users are emailing you about first. Resist. Nobody emails you about the leak.
The security fixes, in practice. Move every secret-bearing API call server-side: a Next.js API route or a Supabase Edge Function that reads the key via Deno.env.get(), so the browser only ever talks to your endpoint with the anon key (/errors/lovable/api-keys-exposed/ shows the Edge Function proxy pattern). Add server-side auth in shared middleware, deny by default, and then add the ownership check on top: after authenticating, verify the requesting user actually owns the resource ID in the request, because changing /api/users/123 to /api/users/124 should not return someone else's data (/errors/vibe-coding/no-auth-on-endpoints/). Replace wildcard CORS with an explicit origin allowlist read from an environment variable, and never combine credentials with a wildcard (/errors/vibe-coding/wildcard-cors/). Convert every string-built query to parameterized queries or an ORM (/errors/vibe-coding/injection-flaws/). Add rate limiting at the edge (Vercel WAF, Cloudflare, or @upstash/ratelimit keyed on IP and user) plus per-user quotas in application logic; in one study of 15 AI-generated apps, only one even attempted rate limiting, and that one was bypassable (/errors/vibe-coding/no-rate-limiting-surprise-bill/).
Now finish the RLS work you started in Step 2 by writing real policies. One policy per operation, per table. For reads: CREATE POLICY "Users read own rows" ON public.your_table FOR SELECT TO authenticated USING ((select auth.uid()) = user_id); INSERT policies use WITH CHECK instead of USING, and use (select auth.uid()) rather than bare auth.uid() so Postgres evaluates it once per query instead of once per row. If file uploads now fail with 'new row violates row-level security policy', that is RLS working but incomplete: Storage uploads need both an INSERT policy and a matching SELECT policy on storage.objects, because the upload reads the row back (/errors/supabase/rls-policy-violation-42501/ covers every variant of that error).
A word on fixing with the AI, since you will use it for some of this. Do not click 'Try to Fix' more than twice; each failed attempt pollutes the context and conditions the next attempt on a more confused history (/errors/lovable/stuck-in-error-loop/). Instead: revert to the last working version, diagnose in Chat mode by pasting the exact error text from the console or the logs, and only then allow one small edit per prompt. If the thread is thoroughly polluted, start a fresh one. For the opaque 'Edge Function returned a non-2xx status code' error, ignore the client message entirely and read the real failure in the Supabase dashboard under Edge Functions > Logs (/errors/supabase/edge-function-non-2xx/). If signups redirect users to localhost:3000, that is not code at all: set your real production URL in Supabase under Authentication > URL Configuration and add every environment to the redirect allowlist (/errors/supabase/auth-redirect-localhost/). Working this way also protects your wallet; debugging spirals are exactly how credits vanish with nothing to show (/errors/lovable/credits-burned-broken-state/).
- Order the fix list: security-critical, data risk, blockers, cosmetics
- Move every secret-bearing API call behind a server-side route or Edge Function
- Add deny-by-default auth middleware plus per-resource ownership checks
- Replace wildcard CORS with an environment-variable allowlist
- Convert all string-built SQL to parameterized queries
- Add edge rate limiting plus per-user quotas on anything that costs money per call
- Write per-operation RLS policies (WITH CHECK for INSERT, both INSERT and SELECT for Storage)
- Cap AI fix attempts at two, then revert, diagnose in Chat mode with exact error text
Step 6: Step 6. Test like an attacker, then like a user
Attacker first, because that is who finds the gaps you miss. Hit every API endpoint with plain curl: no cookies, no headers, no session. Then hit each one again as a logged-in user but with another user's resource IDs. Anything that returns data in either case goes back on the fix list. For Supabase, verify from outside the app: run curl 'https://<project>.supabase.co/rest/v1/<table>?select=*' -H 'apikey: <anon-key>' with no auth and confirm you get only the rows an anonymous user should see, which is usually none. Then create a second account in an incognito window and confirm it cannot read the first account's rows.
Check the CORS fix actually took: curl -s -I -H 'Origin: https://evil.example' https://yourapi.com/endpoint and confirm the response does not echo the attacker origin back. And load-test the rate limiter until it triggers, because a limiter that is defined in a file but never wired into the app passes code review and fails in production; that exact miss shows up repeatedly in audits of AI-generated backends.
Now test like a user. Build locally before you deploy: npm install && npm run build. The first 'Module not found' line tells you which import is broken, and it catches hallucinated component names and invalid package.json entries before the deployment platform does (/errors/v0/module-not-found-export/ if you exported from v0 and the build explodes). Run the full auth flow with a fresh signup, not an old account, because old confirmation emails still contain old redirect URLs. Then click through every core flow once, slowly, with the browser console open.
- Curl every endpoint with no auth, then with another user's IDs
- Curl the Supabase REST API with only the anon key and confirm it returns nothing sensitive
- Confirm a second account in incognito cannot read the first account's data
- Curl with a hostile Origin header and confirm it is not echoed back
- Trigger the rate limiter on purpose and watch it actually block
- Run npm install && npm run build locally and fix the first error, not the loudest one
- Complete a fresh signup end to end with the console open
Step 7: Step 7. Redeploy and watch it like it owes you money
Environment variables first, because they are the most common reason a fixed app breaks again in production. Confirm every variable exists in the hosting dashboard for the Production environment specifically (on Vercel: Project > Settings > Environment Variables), not just in your builder's settings UI. Then redeploy, because values are injected at build time; an existing deployment keeps reading undefined forever no matter what you change in the dashboard. Keep the NEXT_PUBLIC_ prefix strictly for values that are safe in the browser, and keep secrets un-prefixed and server-only (/errors/v0/env-variables-undefined/ covers the whole family of these failures).
If the deploy hangs, do not sit refreshing the builder's UI. Open the Vercel dashboard directly and check the Deployments tab; v0 in particular has a known pattern where the build succeeds but the builder's status never updates (/errors/v0/deployment-stuck/). If the build genuinely failed, open the full build logs, find the first real error line, and fix that specific error. If the platform's deploy pipeline keeps flaking, decouple from it: push the code to GitHub and connect the repo to Vercel directly, which gets you normal build logs and reliable deploys.
After the deploy is green, watch it. Runtime logs (on Vercel: Project > Logs) show the real stack trace behind any 500, which is usually an undefined key crashing an SDK constructor. Check your API usage dashboards daily for the first week; abuse and mistakes both show up as usage curves before they show up as anything else.
- Confirm every environment variable exists for Production in the hosting dashboard
- Redeploy after any environment variable change
- Verify a stuck deploy against the Vercel dashboard, not the builder UI
- Fix the first real error line in the build log, not the summary message
- Check runtime logs for the true cause of any 500
- Watch API usage dashboards daily for the first week
Step 8: Step 8. Prevent the sequel
The apps that end up back in this runbook share the same missing habits, so build them now while the pain is fresh. Git discipline first: commit before every AI session and after every working state, and do agent work on a branch. Git is your undo button, and it works on everything except the things you never committed.
Separate the AI from your production data permanently. Give agents a dev database with its own credentials; the production connection string should not exist in the agent's environment at all. Run agents in plan or approval mode for anything destructive, and deny-list the commands that end careers: DROP, TRUNCATE, DELETE without WHERE, rm -rf, and force-push. The Replit incident that deleted a production database happened during an explicit code freeze; instructions are not guardrails, permissions are.
Then automate the safety net so it does not depend on your memory. Schedule your own database backups (a cron job running pg_dump is enough). Turn on GitHub push protection and add a pre-commit secret scanner (gitleaks or GitGuardian's ggshield), because AI-assisted commits leak secrets at roughly twice the rate of human-only commits and the next generated key literal should die on your machine, not on GitHub. Run SAST (Semgrep or similar) on every AI-generated diff in CI. If you are on a free Supabase project, either upgrade or set up a heartbeat query so it does not pause out from under you again (/errors/supabase/project-paused-app-dead/). And keep the spend caps and billing alerts from Step 2 forever; they cost nothing until the day they save you four figures.
- Commit before every AI session; run agent work on a branch
- Point agents at a dev database only; keep prod credentials out of their reach
- Require plan or approval mode for destructive operations and deny-list dangerous commands
- Schedule automated database backups with pg_dump or platform backups
- Enable GitHub push protection plus a pre-commit secret scanner
- Run SAST on every AI-generated diff in CI
- Add a heartbeat or upgrade to stop free-tier database pausing
- Keep spend caps and billing alerts on every paid API
You shipped something real, fast, with tools that make the shipping easy and the safety optional. Now you know which parts were optional. Run the runbook once and the second pass gets easier: keys live server-side, RLS is on, backups run on a schedule, and the AI works on a branch with a dev database. That is the whole trick. The founders who survive vibe coding are not the ones who never break anything; they are the ones who can restore from yesterday. Be one of those, and the next incident is an afternoon, not a crisis.
Frequently asked questions
Can a vibe coded app be fixed, or do I need to rebuild it from scratch?
Most vibe coded apps can be fixed without a rebuild, because AI tools fail in predictable ways: missing Row Level Security, secret keys in the frontend bundle, unauthenticated API endpoints, and no rate limiting. Those are configuration and architecture fixes, not rewrites. A rebuild only makes sense if a code audit shows the core data model is wrong, and you will not know that until you have done the audit. Work the runbook in order: stop active leaks, back everything up, audit the code, then fix by severity.
How do I know if my vibe coded app has been hacked or is leaking data?
The most common leak in a vibe coded app is silent: a Supabase database with Row Level Security disabled can be read by anyone using the public anon key that ships in your JavaScript bundle, and nothing errors. Run the Supabase Security Advisor (Database > Advisors) to flag tables with RLS off, check every paid API dashboard for usage you do not recognize, and search your inbox for secret scanning alerts from GitHub or GitGuardian. This pattern is real: the scan behind CVE-2025-48757 found about 10 percent of 1,645 public Lovable projects exposing data exactly this way.
What should I do first if my API key is exposed in my app's frontend?
Treat any API key that ever shipped in a JavaScript bundle as compromised and rotate it at the provider's dashboard immediately; removing it from the code does not un-leak it, since copies persist in git history and scraped bundles. Then move the API call server-side into a Next.js API route or a Supabase Edge Function, so the secret lives in a server environment variable and the browser only talks to your endpoint. Finish by setting hard spend caps on every paid API: one documented case of an exposed OpenAI key ended in a $14,000 bill.
What is Supabase Row Level Security and why was it off in my app?
Row Level Security (RLS) is the Postgres feature that controls which database rows each request can read or write, and it is the only thing that makes Supabase's public anon key safe to ship in an app. AI builders like Lovable historically created tables without enabling RLS, the pattern behind CVE-2025-48757, where roughly 10 percent of scanned public Lovable apps exposed user data through their auto-generated REST API. With RLS off, the anon key is effectively an admin key, so enable RLS on every table in the public schema and add explicit policies for each operation.
Will reverting my Lovable or Replit project to an earlier version restore my database?
No. Version history in tools like Lovable restores code only, so database rows deleted by a migration or an AI agent do not come back when you revert the project. Check your database platform's own backup tooling instead: Supabase Pro plans include daily backups, while the free plan has no downloadable automated backups, which makes a manual pg_dump your real safety net. In the well-known Replit incident, the agent claimed rollback was impossible when the data was in fact recoverable from a platform backup, so always verify recoverability yourself.
How much AI-generated code is actually insecure?
Veracode's GenAI Code Security Report tested output from more than 100 large language models across 80 tasks and found that 45 percent of AI-generated code samples introduced OWASP Top 10 class vulnerabilities, with cross-site scripting missed in 86 percent of relevant tasks and log injection in 88 percent. Newer and larger models were not measurably more secure than older ones, because they reproduce the insecure patterns that saturate their training data. That is why auditing a vibe coded app is a standard step, not a sign you did something unusually wrong.
Should I take my vibe coded app offline while I fix it?
Take the app offline only if it is actively leaking data and you cannot stop the leak within minutes, for example a database with Row Level Security off and no quick path to enabling it, or an exposed key you cannot rotate yet. A deliberately dark app costs you a day of users; a leaking app can cost your users their emails, addresses, and stored credentials. If rotating a key or enabling RLS contains the leak, do that first and keep the app up.
How do I back up a Supabase database on the free plan?
The Supabase free plan does not include downloadable automated backups, so you make your own: run pg_dump against your project's connection string, or use the dashboard's CSV export for your most important tables. Put pg_dump on a schedule, since a simple cron job is enough to guarantee a restore point exists before any AI agent touches the database again. Also note that free Supabase projects pause after about 7 days of inactivity and become unrecoverable if left paused past the 90 day restore window.
The Vibe Oops briefing
One email when something ships to production that should not have. New incidents, new error guides, no filler.