How to Take a Vibe-Coded App From Prototype to Production

by Maven Team, Software Development

Vibe coding has changed how fast software gets built. Describe a feature to an AI coding agent (Claude Code, Cursor, Copilot, take your pick), watch it appear, describe the next one, and in a weekend you have a working app, a green test suite, and a demo that does everything you asked. It looks finished.

It usually isn't. What you have is a prototype wearing a product's costume. The gap between "works in the demo" and "safe to put real users and real money through" is the last 30%, and it's the part vibe coding skips, because none of it surfaces until real users arrive.

This is a guide to that 30%: what to look for, why it's there, and how to keep it from accumulating in the first place.

Start with the tests, because they lie

The first place to look is the test suite, and it's counterintuitive: a vibe-coded app usually has plenty of tests. That is not reassurance. It's the first warning sign.

Here's why. TDD, the discipline that makes tests worth anything, writes the test first, as a statement of intent: "this is what the code should do." The code is then written to pass it, and the test fails loudly whenever the code does the wrong thing.

Vibe coding does the reverse. Tests are generated after the code, to match whatever the implementation already does. They assert the current behaviour, bugs included. The buggy behaviour becomes the "expected" behaviour, and the tests pass because they were reverse-engineered from the very code they claim to verify.

So a wall of green checkmarks can verify nothing at all. That is arguably worse than having no tests. No tests at least signals honestly that the code is untested, whereas a green suite signals "safe" when it isn't. And because these tests are usually over-mocked and tautological, asserting only that a mock was called, they break the moment anyone refactors. A good test lets you change code with confidence; these punish you for touching it.

Treat a passing suite on a vibe-coded app as unverified until proven otherwise. Read the tests. If they mock everything and assert nothing about real behaviour, they are decoration.

What "done" is hiding

Read past the demo, the part vibe coding invites you to skip because it works, and a predictable set of problems appears. None of them break the demo. All of them surface a few weeks after real users arrive.

Nothing is DRY. The same data-fetching logic, formatting, and markup get copy-pasted across a dozen files with small variations. Models solve the problem in front of them, in isolation, with no memory that the same problem was solved four files ago. The cost lands later: fixing one bug means finding and fixing it in nine places. The unglamorous remedy is to pull the repetition into reusable utilities, hooks, and components, so there is one source of truth instead of nine.

There are no real abstractions. It is common to find a single component well over a thousand lines doing a dozen unrelated things, with no seams, no clean boundaries to reach for when you need to change one behaviour without disturbing the others. Vibe coding produces breadth (lots of features) with no depth (no structure underneath), so every new feature fights the ones before it.

The types are decorative. any scattered throughout, or loose shapes that quietly misrepresent the data. TypeScript that doesn't type anything is just ceremony.

State is scattered across dozens of useState calls and prop-drilling, with no coherent idea of where the source of truth lives.

The data layer doesn't exist. Fetches sit inline in components, happy-path only, with no loading, empty, or error states, because the demo never fails, so the model never wrote for failure. Real users fail constantly.

Security is the one that bites hardest. Access rules enforced in the UI and nowhere else. Row-level security left effectively off. Configuration and secrets drifting toward the client. None of it matters in a demo with a single user. All of it matters the moment a second person, a real payment, or someone else's data enters the picture. An independent codebase audit exists to catch exactly this, before the users do.

How to do it right: put the discipline back in front

None of this is an argument against vibe coding, or against the tools that enable it. The speed is real and worth having. The problem is narrow: vibe coding removes the friction that used to force engineering discipline to happen first. Put it back deliberately and you keep the speed without the debt. Three practices do most of the work.

1. Make tech debt physically unable to land

Neither the model nor a tired human at 11pm will reliably enforce the rules by willpower. So move enforcement into tooling that never gets tired, and run it at more than one gate.

The off-the-shelf baseline is worth having: a copy-paste detector (jscpd) that fails the build on duplicated blocks, import-boundary rules (eslint-plugin-boundaries, import/no-restricted-paths, dependency-cruiser) that stop one module reaching into another's internals, TypeScript strict, and Husky pre-commit hooks so none of it reaches the repo.

The bigger wins come from rules specific to your codebase, not the generic ones. Keep a small catalogue of "you just re-implemented something the codebase already has a primitive for" checks, and enforce that catalogue in three places:

  • As the AI writes. A hook that fires after each AI edit (a PostToolUse hook in Claude Code, or the equivalent in your tool) scans the file just touched, and when it spots a re-implemented pattern it tells the model exactly which sanctioned helper to use, and where. The agent sees that on its next turn and self-corrects, so the debt is caught as it is written, not discovered weeks later.
  • When anyone commits. The same catalogue runs in the pre-commit hook, so it bites human commits identically. AI or not, the rules are the rules.
  • As a whole-tree backstop. An enforcement test scans the entire codebase on every commit, catching drift a per-file check would miss, like a rename or a copy into a new folder.

What sits in that catalogue is domain-specific, but the shapes recur in every project. A few, translated to the kind of thing worth blocking:

  • A raw modal-and-form built from scratch when a shared dialog component already exists for one-shot create and edit actions.
  • A confirmation prompt hand-wired instead of composing the standard "are you sure?" component.
  • A table cell formatting currency, a percentage, or a status pill inline, when a column factory already does it consistently everywhere else.
  • A feature file importing the HTTP library directly instead of the configured client, quietly bypassing token refresh and response unwrapping.
  • A list endpoint reading pagination parameters by hand instead of the shared parser, or returning data without validating it against a schema first. Silent shape drift is how a backend breaks a frontend with nobody noticing.
  • A request handler opening a database connection directly instead of going through the data layer, which leaks connections under load.
  • A : any slipping in, a stray console.log, a hardcoded colour instead of a design token.

Two habits make the catalogue pay off. First, add a rule the moment you fix the same thing twice; one good primitive plus two ad-hoc rivals is exactly how a fourth one gets written. Second, when an exception is genuinely warranted, keep an explicit, dated allowlist that new code is forbidden from joining, so old debt is tracked without the rule going quiet on everything written since.

The principle underneath all of it: make the wrong thing impossible, not merely discouraged.

2. Define the requirements before writing code

Vibe coding is reactive by nature, a steady "now add this, now add that." But architecture is a function of knowing where you're going. Discover the destination one prompt at a time and you never get to design the data model or the shared abstractions once, coherently; you just accrete features, each fighting the last. Decide the shape of the whole thing first. It is the difference between a codebase and a pile.

3. Use plan mode before writing code

Most agentic coding tools have a plan mode, Claude Code included, and it exists for a reason. Before a line is written, have the tool lay out the approach, covering the file structure, shared utilities and components, data flow, and boundaries, then pressure-test that. The plan is the one place the DRY and abstraction decisions are still free to change. Once they are code, they are expensive. It is always cheaper to rewrite a paragraph than a module.

The bottom line

AI didn't remove the need for engineering. It removed the friction that used to force the engineering to happen first. Building by hand was slow enough that you had to think before you typed; vibe coding is fast enough to skip straight to typing, so the speed carries you into the wall faster.

The fix is to put the thinking back in front, deliberately: requirements, then a plan, then guardrails. And the useful twist is that the same tooling that creates the mess is what enforces the discipline. An AI agent with hooks, a plan mode, and a well-maintained context file (a CLAUDE.md, or your tool's equivalent) is a different animal from the same agent pointed at an empty repo and told to "just build it."

That is the real division of labour. AI writes the 70%. Turning it into the 30% that survives contact with real users is engineering.

More articles

TypeScript Patterns We Actually Use (And the Strictness That Earns Its Friction)

Not every TypeScript feature is worth the friction, and not every strict-mode flag earns its place. Here are the patterns a React and Next.js shop actually leans on day to day, the compiler settings worth the noise, and what TypeScript 7, the native Go compiler that went GA in mid-2026, actually changes about adopting all of it.

Read more

How Do You Actually Test an AI Feature? Evals for Teams Shipping LLM Products

Unit tests assume the same input gives the same output. LLMs break that assumption on purpose. Here is how teams actually test AI features, building an eval set, LLM-as-judge, regression testing in CI, and what "good enough to ship" really means.

Read more

Bring us the messy, valuable software problem.

We will help you decide what to build, what to modernise, and where the highest-return work actually is.

Our offices

  • London
    71-75, Shelton Street,
    Covent Garden, London