Guide · Vibe-coded apps

Vibe-coded apps: what breaks after launch

A vibe-coded app is not badly written bad code. It is written fast, in a browser tab, against a demo that worked. The mistakes that survive to production are surprisingly consistent, and they cluster in six places: what ships to the browser, who is allowed to read which row, which numbers the browser gets to choose, what is left open to the internet, what happens when money is involved, and what happens when something fails halfway.

Written by the reviewer at CodeReviewPro · the same checks as in the sample report

01 / The pattern

Why the same failures keep appearing

The demo is the specification

Tools like Lovable, Bolt, v0, Cursor and Claude Code are excellent at making an app that works when the person who built it clicks through it. That is also the only acceptance test a generated project usually gets. Whoever wrote the prompt clicks the happy path, sees the right screen, and ships it. The code was never asked to survive a stranger.

Only the happy path gets written

Failure handling is invisible in a demo, so it is the first thing that quietly disappears. The catch block stays empty. The charge that succeeded but did not write the order stays unhandled. The retry that can double-charge is never written. None of it shows up until real money or a real customer is in the flow.

Fluent code gets skimmed

Generated code reads well: consistent naming, plausible structure, comments that match the intention. That fluency is exactly why the review goes wrong. A reader who skims sees code that looks like what they asked for, and the missing permission check on line 212 does not look out of place.

The result is not exotic breakage. It is the boring kind: a key in the front end, a price taken from the browser, a table anyone signed in can read, an endpoint anyone can call in a loop.

02 / The checks

Eight checks for a vibe-coded app

Each one is a question you can answer with a search and a read. The commands assume a JavaScript project with Supabase or Firebase and Stripe; the idea carries over to other stacks.

  1. 01

    Keys that ship with the front end

    There is a category of key that is meant to be public (a publishable key, a map token with a referrer limit) and a category that is not (a service role key, a secret API key, a mail password). Vibe-coded projects mix them up constantly, because the fastest way to make something work is to put the key where the failing call is. Anything a visitor can load, a visitor can read.

    check the build, not the source
    # what does the browser actually download?grep -rniE "service_role|sk_live|secret|private_key|mail_password" dist/ build/ public/ .next/static/
  2. 02

    Row-level security that was never switched on

    Supabase and Firebase both give you per-row access rules, and both let you ship without them. A table with rules disabled and a public API key is readable by anyone who opens your app in a browser, including the rows of other users. This is the single most common finding in apps built this way, and it is invisible in every screen of the app itself.

    ask the database, not the interface
    -- tables where RLS is off, or on with no policy attachedselect relname, relrowsecurity from pg_class where relkind = 'r' and relnamespace = 'public'::regnamespace;
  3. 03

    Numbers the browser gets to choose

    If the request carries the price, the quantity, the discount, the credits or the user id, then the browser decides it. The server has to look the value up itself and use that. The tell is a handler that reads a price or a total out of the request body where it should have read the row it just fetched, and it is easy to miss because the field is named exactly like the database column.

  4. 04

    Endpoints anyone can call in a loop

    The demo needs one call. Production needs a limit on how often, from where, and with what payload. An endpoint that writes a row, sends mail or calls a paid AI model with no rate limit is an invitation: one script, ten thousand requests, your invoice. The same routes usually skip size limits as well, so a very large body is another way in.

    test it logged out, from outside
    curl -i -X POST https://your-app.example/api/send -H "content-type: application/json" -d '{}'# a 200 here means the route has no idea who is calling
  5. 05

    Storage buckets left open

    Uploads are the other half of the rules: the storage layer has its own permissions, separate from the database. A bucket set to public, or with permissive write rules, lets people read files that were never meant to be readable, and lets strangers use your storage as free hosting. Check read, write and list separately; they are three different permissions.

  6. 06

    Webhooks nobody signed

    A payment webhook is a public URL that marks orders as paid. If the handler does not verify the provider's signature, anyone who finds the URL can post to it and mark an order paid without paying. Verify the signature against the raw request body, and make sure the same event arriving twice cannot deliver the same thing twice.

  7. 07

    The money path, read as a story

    Follow one purchase from the button to the confirmation mail and ask four questions. Can the same request be replayed and charge twice? What happens if the charge succeeds and the write after it fails? Is the stock or credit checked before the charge or only after? And can two buyers take the same last item at the same moment? Every one of those answers is a finding or a clean bill of health.

    failures that fail silently
    try { await charge(card) } catch (e) {}  // charged, order unknown, customer lost
  8. 08

    Deletes, debug mode and headers

    Three small ones that cost nothing to fix. Deleting a row that other rows point at, with nothing to undo it. A debug mode or stack trace left on in production, which tells a stranger how your app is built. And missing security headers, which is the first thing any internet-wide scanner reports about you, before anyone ever looks at your code.

03 / Try it yourself

An hour of testing that finds most of it

You do not need a security background for these five. You need a second account and a curious mood.

  1. 01

    Make a second account and try to read the first one's data

    Log in as user B, then change an id in the URL, in a form field or in the API call to an id that belongs to user A. If the data comes back, the permission check lives in the interface and not in the server.

  2. 02

    Call your own API while logged out

    Open the network tab, copy one request that writes something, paste it into a terminal without the login token. A route that answers anyway is a route that trusts the caller.

  3. 03

    Search the built files for key shapes

    Run the grep above on what the browser downloads. Every hit is either a key that is meant to be public or a leak; the difference is worth knowing before someone else finds out.

  4. 04

    Change a price in the request and watch the total

    On a checkout flow, edit the amount in the request before it goes out. If the order is created at your number, the server never owned that number.

  5. 05

    Turn something off and see what breaks

    Disable the mail provider key or point one dependency at a dead address, then place an order. What you get is your real error handling: a page that lies about success, a blank screen, or an honest message. That is what a customer would have seen anyway.

Most of that is reading rather than tooling. What makes it hard is doing it on your own project, where you already know what the code was meant to do, and your eyes skip the parts that match your intention.

04 / Straight answers

Questions that come up before a review

Does a review mean my app is unsafe?

No. Most of what I report are the ordinary mistakes of shipping fast: a missing check, a value in the wrong place, an unhandled failure. A review tells you which of them are in your code, with the file and line, and which parts were actually fine. "Nothing serious found" is a normal and useful outcome.

Is it worth reviewing an app nobody has attacked yet?

That is exactly the moment it is cheapest. Before launch you fix things while you still remember the code and before anyone has your data. After launch you are fixing them while also answering a customer and possibly a payment provider.

What do you need from me?

A repository, a zip, or the files that matter: routes, database access, the payment code, the configuration and the deployment settings. Read-only access is enough. I do not need your production database, live keys or customer data, and I do not want them.

Which tools do you run?

Dependency and secret scanning, plus a manual read of the routes, the permission rules and the money path. The scans find patterns; the reading finds the mistakes that make sense in your app and nowhere else. Both are in the report, and each finding comes with what I did to confirm it.

Can you fix what you find?

If you want, yes, at $120 per hour after the report, with the hours estimated before I start and your approval first. If you would rather fix it yourself, the report is written so that your own developer can work straight from it.

05 / Next step

Send your code, get a first look free

Send a repository, a zip or the files that matter, and you get the first findings for free: up to three, if I identify them, with file, line and evidence. If nothing serious turns up, you get that answer instead. A full review is a fixed price: Quick check $299 or Deep check $799, with repairs after the report at $120 per hour if you want them done. The first two clients pay $149 instead of $299.

free first look · up to 3 findings · no subscription, no retainer

Send me your code Read the sample report

Email works too: . Please do not send passwords, live keys or customer data.