Prototype → production

Your vibe-coded app works. Now you need to be able to trust it.

By Louis CassedanneProject Lead & Data and AI Architect

Published on · 13 min read

You built a first version with Lovable, Bolt, Cursor, Replit, Claude Code or another coding agent. It works well enough to demonstrate the idea, convince a customer, or start serving real users.

At that point the question is no longer really how the code was produced. What matters is whether you understand the system well enough to trust it in production.

Where the code came from does not answer that on its own. AI-generated code can be correct; code written entirely by hand can be fragile. What vibe coding mainly changes is the speed at which your product can become functional without your understanding of it keeping pace.

You can end up with an application that is close to complete as a piece of product, and still a long way from the guarantees it needs to be operated: permissions, data integrity, error handling, observability, cost control, the ability for someone else to take it over.

That gap is the subject here. Moving to production is less about "cleaning up" the code than about turning a system you understand imperfectly into one whose risks you understand well enough.

The real subject: comprehension debt

Technical debt did not arrive with AI assistants. You can already inherit a poorly documented codebase, take over a contractor’s work, or discover decisions nobody fully owns any more.

What vibe coding sharpens is something more specific: you can produce far faster than you build a mental model of the system.

In more traditional development, part of that understanding is built by writing the code. When you ask an agent for authentication, a customer area and a payment system, you can get a convincing demo very quickly without having answered questions like:

  • where are permissions actually checked?
  • can a user reach another organisation’s data by editing a request?
  • what happens if a webhook arrives twice?
  • can a migration leave existing data inconsistent?
  • which third-party services were added along the way?
  • what happens if a model call takes thirty seconds, or costs a hundred times more than expected?

None of this means your prototype is bad. These questions were simply outside what it was built to answer.

GitHub makes the same point in its Copilot documentation: generated code can look valid while being incorrect or vulnerable, and must be reviewed and tested before it is merged (GitHub Docs).

A prototype proves a use. Production asks for guarantees.

A prototype sets out to check that a use is worth existing. In production you also have to guarantee the product keeps working as conditions get less favourable: more users, more data, a third-party service down, a mistake in an operation, a version change.

Those guarantees are not absolute. An internal tool used by five named people does not carry the risk profile of a public multi-tenant SaaS. The NIST Secure Software Development Framework works the same way: practices are meant to be adapted to context, risk tolerance, resources and cost.

A prototype is mostly judged on what it shows. In production you are also judged on what your product keeps guaranteeing once conditions degrade.
DimensionPrototypeProduct in production
UseThe main path worksThe error cases that matter are handled
AccessA few known usersPermissions are explicit and tested
DataIt is storedIt is protected, migratable and recoverable
Third partiesThey respond during the demoTheir outages and retries are planned for
OperationsYou find out about problemsYou can detect and diagnose them
HandoverThe author knows the systemSomeone else can understand and maintain it

Prototype

Demonstrate

"Is this use worth existing?"

Product in production

Guarantee

"Does it hold when conditions degrade?"

The table above details six dimensions; they all say the same thing. What changes in production is not the amount of work, it is the nature of what you are asked to prove.

In practice — Themis-X / MyTravelConnect

For Themis-X we designed and built MyTravelConnect end to end. A designed and prototyped first version shipped in April 2024, the full application was online in May, and the engagement ran until September, with a proof of concept built with Aéroports de Paris and then a recommendation layer.

The product was not open to the public. Known participants in the EONA-X ecosystem connected to it to exercise specific use cases. That exposure was real, and known in advance, which is what made it possible to calibrate the guarantees of the first deployment.

But "controlled exposure" does not mean "lighter guarantees everywhere", and that is the interesting part. What the context allowed us to defer was scale: connecting the whole travel industry, self-service onboarding, load. What it did not allow us to defer was consent and data protection — because that was precisely what the product had to demonstrate.

Before refactoring, rebuild the model of the system

When you take over an application you barely know, the first instinct is usually to open the repository and look for what seems messy. That is not the best starting point.

Start instead by understanding:

  • who uses the system, and in which roles;
  • where the sensitive data lives;
  • which external services are involved;
  • which work happens asynchronously;
  • which actions are expensive, irreversible or hard to replay;
  • where the level of trust changes.

That last one matters most. In threat modeling, OWASP recommends mapping data flows, external actors and trust boundaries — the points where data moves from somewhere you control what happens to somewhere you no longer do — before reasoning about threats at all.

Take a SaaS application with a frontend, an API, PostgreSQL, Stripe and an external LLM. The repository shows you modules. A risk map shows you something else: the points where data moves from the browser into the API, from a user to a tenant’s records, from your backend to Stripe, or from your database to an external model.

Outside your control

User

Role and organisation to establish, never to believe

Browser

Everything arriving from here is an assumption

Authentication

Your application

Frontend

No check can stop here

API

Where permissions must actually be verified

Workers

Asynchronous work, replayable

Per-tenant authorization

Your data

PostgreSQL

Personal data, migrations, backups

File storage

Direct access to control separately

Secrets

Keys to the services below

Data leaving

Third-party services

Payments

Inbound webhook: authenticate it, make it idempotent

LLM

What leaves does not come back; variable latency and cost

Email

A tolerable outage, provided you know about it

A useful map does not have to be exhaustive. It has to show where a mistake can have real consequences. The two shaded bands are the ones you do not control: everything between them is where a check can exist.

Those boundaries are where it becomes worth asking which checks actually run and which assumptions are implicit. An 80-line function is rarely a risk in itself. An implicit authorization rule can be.

Secure the invariants that matter

Once you understand the system, reading the whole codebase at the same intensity is rarely the best use of the time available.

It is more useful to start from the invariants: the properties that must stay true.

In a B2B SaaS:

A user from company A must never be able to read or modify company B’s data.

In a payment system:

Receiving the same event twice must never produce the business effect twice.

For an external integration:

A third-party outage may degrade a feature, but must not make the whole application unusable.

From there you can build the controls that genuinely buy confidence. For tenant isolation, an explicit test matrix is usually more useful than an abstract coverage target:

An authorization matrix makes the invariant testable: every row is an integration test that has to pass, including the ones whose expected result is a refusal.
ActorResourceActionExpected result
User AInvoice AReadAllowed
User AInvoice BReadDenied
Admin AUser BUpdateDenied
AnonymousInvoice AReadDenied

Broken access control is still the first category in the OWASP Top 10 2025, and OWASP publishes a dedicated guide to authorization regression testing.

The same logic applies to idempotence: Stripe documents explicitly that the same webhook event can be delivered more than once, and recommends recording the events you have already processed (Stripe Docs).

So the question is not "do we have enough tests?" but which important properties are we able to defend?

Prioritise risk, not code tidiness

A codebase can be imperfect without being dangerous. Conversely, a very tidy application can hide a critical business assumption that has never been tested.

For each sensitive area, look at least at five things:

These five questions belong together: a low likelihood with a wide blast radius is not handled like a high likelihood with a reversible impact.
QuestionExample
How likely is failure?An unstable external service, a migration run by hand
What would the impact be?A duplicated payment, a data leak
How far would it reach?One user, one tenant, every customer
How fast would you know?An immediate alert, or a customer complaint
Can you recover?A simple retry, or corruption that is hard to reverse

Likelihood counts, but it does not decide on its own: on a security invariant, reach usually wins, because a small but non-zero likelihood applied to every one of your customers is still unacceptable.

A cross-tenant permission is checked incorrectly

Fix immediately

Impact
Another customer’s data leaks
Reach
Potentially every customer
Detection
Hard — nothing flags a read that was wrongly allowed

A payment webhook runs twice

High priority

Impact
Double charge, or double fulfilment
Reach
One customer at a time
Detection
Medium — usually via the complaint

A UI component is duplicated

Can wait

Impact
Visual inconsistency
Reach
One screen
Detection
Easy — visible straight away
Three defects a tidiness-driven review would rank in the opposite order: the third is the only one you can see by opening the repository. What separates the first two is not their likelihood, it is how far they reach and how long it takes to notice.

Before production we almost always look at authorization, tenant isolation, secrets, migrations, data integrity, idempotence, external dependencies, backups, variable costs and observability.

Non-critical duplication, inconsistent conventions, mediocre naming or purely cosmetic refactors can usually wait.

The OWASP Top 10 2025 also puts Software Supply Chain Failures in third place, which is reason enough to take dependencies and the build chain seriously on a quickly assembled prototype.

Put three kinds of guardrail in place: prevent, see, recover

Once the priority risks are identified, choosing guardrails becomes much simpler.

Prevent

  • Tests on the invariants
  • CI and type checking
  • Explicit access rules
  • Versioned migrations

See

  • Structured logs
  • Error tracking
  • Metrics
  • Actionable alerts

Recover

  • Rollback
  • Tested backups
  • Feature flags
  • Retries and kill switches
Production readiness is not about making failure impossible. It is also about making it visible and recoverable.

Prevent

CI, type checking, tests on the invariants, static analysis, secret scanning, versioned migrations, explicit access rules.

The aim is not to prevent every bug, but to keep out the mistakes that are already predictable enough.

See

Structured logs, error tracking, metrics and alerts someone can actually act on.

Google SRE offers four useful signals for user-facing systems: latency, traffic, errors and saturation (Google SRE). On an AI product you can add timeouts, retries, or the average cost of a journey.

Recover

Rollback, feature flags, tested backups, retries, and the ability to switch an integration off quickly.

Rewrite or stabilise? Start with whether the original assumptions still hold.

The rewrite question comes up almost every time you take over a prototype. Our position is not "never rewrite".

Two Vezero engagements show the two possible answers.

What separates the two decisions is not the state of the code, but what the product had become in the meantime.
CriterionKyutaiGenerous
Product assumptions in the existing codeStill validSuperseded
What had changedThe intended use: demos, then open sourceThe business goal and the user experience
DecisionKeep and extendRestart on a fitting base

Kyutai: keeping what already had value

On Invincible Voice the existing codebase encoded the right assumptions: the functional core matched the product Kyutai wanted to release. We had no reason to rebuild it.

The work was to extend it towards two deadlines: demos in the short term, then an open-source release — which moves the bar considerably, because published code has to be picked up by developers outside the team. Security and authentication, tests, handover readiness and clearing out dependencies on the original setup all belonged to that second goal.

The Kyutai conversation interface as delivered to production.
On Kyutai, the functional core was kept and extended; nothing that worked was rewritten.

A concrete example of the kind of decision that involves: audio streams were loaded by prefetch, which suited a research prototype perfectly but weighed on how responsive the interface felt. We moved them to streaming. That trade-off does not show up when you read code looking for what is messy — it comes from the intended use.

Two months after the engagement started, the project could be published.

Generous: restarting because the product had changed

On Generous the situation was different. A first prototype existed, but the business goal and the intended user experience had moved on since it was built.

Keeping it would partly have meant keeping assumptions that no longer held — which is exactly the case where a rewrite is justified. We restarted on a base that fit the product actually being aimed at: two months to a demonstrable V1, then two more to full production support.

Keep using AI, but change the decisions it gets to make

Moving to production does not mean dropping Cursor, Claude Code or other agents. What changes is the nature of the instructions you give them.

While prototyping:

"Build me a customer area with authentication and organisation management."

Once the system is critical:

"Here is the current data model. Here is the tenant isolation invariant. Here are the integration tests that encode it. Change only the authorization module. List the migrations needed and the missing tests before writing any code."

In the first case you are asking the tool for a feature. In the second you are asking it for a change under constraints.

GitHub recommends a similar approach: use tests and automated analysis, check that the change respects the project’s architecture, and give the AI sources of truth such as documentation or the repository’s conventions (GitHub Docs).

Vibe codingAI-assisted engineering
"Make this feature work""Change this behaviour under these constraints"
Partial contextExplicit architecture and sources of truth
Mostly visual validationExplicit tests and invariants
Wide latitude given to the toolLimited surface of change
The point is not to use AI less, but to define more precisely what it is allowed to decide.

Three questions to know where you stand

There is no universal production readiness score. But for a given scope, you should be able to answer three questions:

What can break? How will you know? What will you do when it happens?

They map exactly onto the three guardrails above: prevent, see, recover.

If you can answer them for your critical areas, and those answers are tested rather than assumed, your product can be ready even if the code still has rough edges. Conversely, a spotless repository is not enough if nobody can answer them.

Frequently asked questions

Do you have to audit every line of AI-generated code before production?

No. Start by understanding the architecture, identifying the critical flows and writing down the invariants that must never break.

Code review and analysis tooling can then be concentrated where a mistake would have real consequences.

Is AI-generated code less safe than code written by hand?

Where it came from does not answer the question. Generative tools can produce incorrect or vulnerable code, but code written by hand can have exactly the same defects.

For a production decision, it is more useful to assess the controls, the dependencies and the actual behaviour of the system.

How do you know whether to rewrite or keep a prototype?

Look first at the assumptions it embodies. If the business need, the UX and the fundamental constraints still hold, stabilising what exists is usually the rational choice.

If those assumptions have changed deeply, keeping the code can cost more than restarting on a base that fits.

How long does it take to move a prototype into production?

There is no universal duration, but the product’s real exposure changes the problem substantially.

In our own projects we had two months to make Invincible Voice publishable as open source at Kyutai, four months to take Generous from an inherited prototype to full production support, and around two months to put the complete MyTravelConnect application online in a controlled production context.

These are not benchmarks: they mostly show why you have to understand the level of guarantees expected before putting a number on the work.

Method and sources

This article draws on our experience taking over and productionising existing products — Kyutai, Generous and Themis-X / MyTravelConnect in particular — as well as on several external technical frameworks and references.

Your prototype works. The question is what it still needs to hold in production.

We take over what exists, identify the risks that genuinely matter, and work out what has to be stabilised, reinforced or replaced.

The goal is not to rebuild your product "properly" on principle, but to keep what still rests on sound assumptions and replace only what warrants it.

Move your prototype to production