A founder builds an application with Cursor, Lovable, Bolt, or a long conversation with an LLM. It works. It looks good. It handles the demo flawlessly and the first ten users don’t notice anything wrong.
Then real usage starts, and things that never came up in the prompt start mattering: concurrent writes, a user who submits a form twice, a crawler hitting an unprotected endpoint, a database that has grown past the point where a full table scan is free. None of this means the founder did something wrong. It means they built a prototype, and prototypes and production systems are optimized for different things.
This is the situation I get called into most often now, and it has a fairly consistent shape. Here’s what actually needs to happen, in the order it needs to happen.
What vibe-coded apps have in common
Not every AI-generated codebase looks the same, but after enough of these engagements, patterns repeat:
No boundary between trust levels. Client-side checks stand in for server-side authorization. A route handler trusts a userId field in the request body instead of deriving it from the session. This isn’t a one-off bug — it’s the default output of a tool that optimizes for “the feature works when I click through it.”
Secrets in the code. API keys and database credentials committed directly, because environment variable management is a step nobody thinks to ask the AI for, and the AI doesn’t volunteer it.
A schema that grew by accident. Tables added as features were added, no constraints, no indexes beyond primary keys, nullable columns standing in for what should be enums or foreign keys. It works at 200 rows. The first slow query report comes in around 50,000.
Errors that disappear. Try/catch blocks that swallow exceptions and return a generic 200, because that’s what made the error message stop showing up in the UI during development. In production this means failures are invisible until a user complains.
One deployment path: whoever has access, pushes. No CI, no staging environment, no rollback plan. “Deploy” means running a script from a laptop.
A codebase that only the prompts understand. Naming and structure that made sense as an incremental accretion of AI-generated patches, not as a design. Reading it top to bottom doesn’t tell you how it’s supposed to work — you have to run it and watch.
None of this is a criticism of AI coding tools. They are extremely good at producing something that works right now. They have no mechanism for asking “what happens when this runs 10,000 times a day against a database three orders of magnitude larger,” because nobody asked them that question.
Resist the urge to rewrite
The instinct of a lot of engineers, dropped into this kind of codebase, is to throw it out and start over with “real” architecture. This is almost always the wrong call, and it’s worth being explicit about why: the application already validated something a rewrite can’t — that people want to use it. A rewrite pauses that validation for weeks or months, produces nothing releasable in the meantime, and re-introduces every bug the shipped version already had shaken out through actual usage.
The job is closer to structural engineering on an occupied building than to demolition. Find where the load-bearing risk actually is, reinforce it without taking the building down, and keep it usable the whole time.
The order that matters
Fixing things in the wrong order wastes effort and leaves real risk exposed longer than necessary. The sequence that works:
1. Find out what’s actually broken before changing anything
A full read-through of the authentication flow, the data model, the dependency list, and the deployment process — before touching code. Rank findings by blast radius, not by how ugly the code looks. A messy component that only affects the settings page is not the same priority as a broken authorization check on the payments endpoint. This step also produces something the non-technical founder can act on independently, even if the engagement stops here: a prioritized list of what’s actually dangerous versus merely inelegant.
2. Close the holes that could cause an incident
Authentication and session handling get locked down. Authorization checks move from “assumed” to “enforced,” server-side, on every request that touches another user’s data. Secrets move to environment variables or a secrets manager and get rotated. Input validation goes in at the boundaries — the places an AI tool typically didn’t think to add it because the happy path didn’t need it. Dependency vulnerabilities get patched. Backups get turned on, if they weren’t already.
This phase is not about making the code beautiful. It’s about removing the ways a stranger on the internet, or a bug under load, could cause real damage.
3. Make the code survivable by more than its original author
Refactor the pieces that are both fragile and frequently touched — not everything, just what future changes will actually go through. Add tests around the critical paths: checkout, auth, anything involving money or irreversible actions. Clean up the database design where it’s actively causing problems, not everywhere it could theoretically be nicer. The target isn’t an idealized architecture; it’s a codebase a second developer could be handed without a multi-week ramp-up.
4. Build the operational floor
CI so deployments are a pipeline, not a person with laptop access. Separate environments so “testing in production” stops being the default. Monitoring and alerting so a failure surfaces as a page, not a support ticket three days later. Logging that lets you reconstruct what happened after the fact instead of guessing.
5. Scale only what needs it
Once the above is in place and there’s real usage data, address the specific bottlenecks that traffic actually reveals. Not before. Scaling infrastructure for load that doesn’t exist yet is its own form of waste — it adds operational complexity that has to be maintained for no current benefit.
Working with the person who built it
The technical work is the easier half. The harder part is that the person who built the application usually isn’t an engineer, and the fixes need to be explained in terms they can act on — risk and cost, not code quality.
“This authorization check is missing” doesn’t land. “Right now, any logged-in user can view any other customer’s invoices by changing a number in the URL” does. Framing every finding as a concrete scenario, not an abstract best practice, is what turns a technical review into a decision the founder can actually make.
It also means not imposing engineering preferences that don’t serve the business. An MVP with 200 users doesn’t need a microservices split, a message queue, or a Kubernetes cluster. It needs the specific things that are actually putting it at risk, fixed, and nothing more.
The gap AI doesn’t close
AI tools changed who can produce working software. They didn’t change what production requires: security discipline, data integrity, observability, and a deployment process that doesn’t depend on a specific person’s laptop. That gap doesn’t close itself, and it’s not a knock on the tools or the person who used them — it’s just a different discipline, applied at a different stage.
This is exactly the gap the AI to Production engagement is built to close: an assessment of what’s actually at risk, then stabilization, engineering, and productionization in an order that keeps the application shippable the whole way through.
You did the hard part by getting something real into people’s hands. Getting it to stay there is a solvable problem — it just isn’t the same problem.