Case study · Paid media

A vibecoded app
got too big to keep vibecoding.

An agency product for running paid media was built by prompting, and for a while that was enough. Within about three months the codebase was past the size a prompt can hold. Every further step got harder, and progress started arriving with regression bugs. It could keep moving only under an LLM operator — a person directing the model, with the tests and the boundaries of the system in the loop.

Industry
Agency software · paid media
Engagement
Operating a vibecoded codebase
How it was built
Prompted, until the prompt could not see it
Timeline
~3 months to outgrow vibe coding
1,285
PHP files
The size a prompt could no longer hold
360
Test files
Inside that count
34
Domain models
Campaigns, billing, tenancy, content
~3 mo
To hit the ceiling
From a generated app to this one
The starting point

Vibe coding got it here. Then it stalled.

The product is real. An agency or an in-house team connects ad accounts, asks an AI chat about the campaigns, and runs dashboards, billing, client intake, and a content library from one Laravel and React application. Prompting built that. Prompting also built it fast enough that, in about three months, no single prompt could see the whole thing.

That is the point where vibe coding turns on the app:

  • The codebase outgrew the prompt. 1,285 PHP files, 34 domain models, 89 migrations, connections out to the major ad networks. A model asked to “add the next feature” is looking at a window, not at the system.
  • Progress and regressions arrived together. A change that looks local touches tenancy, billing, campaign structure, and the tools the chat is allowed to call. The last feature is what the next prompt breaks.
  • Each step got harder than the one before. The early prompts had little to collide with. Later prompts collided with everything the earlier prompts had already invented.
  • The tests were there, and still not in the loop. 360 test files existed. A vibe-coding session that does not run them, and does not know which boundary it is crossing, ships the regression anyway.
What changed

An operator, not a bigger prompt

The app did not need to be thrown away, and it did not need another round of generating the whole product. It needed an LLM operator: a person who directs the model at the part of the system a change actually touches, and who holds the tests, the types, and the boundaries while the model writes.

  • The model works on a slice. Campaigns, a dashboard, a billing rule, a chat tool — one change, against the code that change can break. The rest of the 1,285 files stay out of the prompt.
  • The suite is part of the workflow. A change is finished when the tests that cover it pass, not when the screen looks right in the session.
  • The boundaries are explicit. Tenancy, what the chat is allowed to call, how money moves. An operator knows which of those a prompt is about to cross. A fresh session does not.
  • The product kept growing. Ad accounts, agent jobs, dashboards, contracts, landing pages. The same app, still getting larger, now able to take a change without giving one back.
Before & after
 Still vibecodingUnder an operator
How a change happensA prompt at the whole appA directed change, on one slice
What the model seesWhatever fits in the windowThe part the change can break
RegressionsThe cost of the next featureCaught by the suite before they ship
Memory of the systemThe last chatTests, types, and the boundaries
Size1,285 PHP files, and climbingStill climbing, and still changeable
Who is responsibleThe toolA person directing the model
The pattern

Every vibecoded app hits this

Vibe coding is a fast way to get an idea into software. It stays fast while the codebase is small enough for a prompt to hold. Past that, each new prompt has more existing behavior to collide with, and the collision shows up as a regression. The app gets harder to move the larger it gets. That is not a bad week. It is the shape of the method.

Operator
is what a vibecoded app needs once it is too big to keep vibecoding. A person directs the model. The tests and the boundaries of the codebase decide whether the change is allowed to land.

What that looks like on an app of this size:

  • Stop prompting the whole repository. The model gets the slice, the convention, and the tests that cover the slice.
  • Treat a regression as a failed change. Green tests are the gate. A screen that looks done in the chat is not.
  • Keep a person in the loop who knows the system. The operator is the memory the prompt does not have: which boundary this change crosses, and which test would notice if it crossed the wrong one.

The chapter before this one is how a vibecoded prototype becomes a production product in four working weeks. This is the chapter after: the product keeps growing, and prompting it whole stops being a way to make progress. Read that story.

The impact

The app could change again

The product stayed. Paid media, the chat, the dashboards, the billing — still one Laravel application, still growing. The difference is that a change is now an operated change. The next feature does not have to cost the last one.

1,285
PHP files, past the prompt
The size at which vibe coding stops being a safe way to add behavior.
360
Test files in the loop
The suite is how an operator catches the regression a prompt cannot see.
~3 mo
From a generated app to this one
Fast enough to build. Fast enough to outrun the method that built it.

File counts are from the repository: 1,285 PHP files, of which 360 are test files, plus 34 domain models and 89 migrations. About three months is the life of that repository up to this size. We are not publishing a measured regression rate. The pattern is what the size makes likely: a prompt cannot see the whole system, so progress and breakage arrive together until an operator is in the loop.

A vibecoded app that has started breaking itself?

Past a certain size, another prompt is how the regressions get in. An operator is how the app keeps moving. Fifteen minutes is enough to tell which one you are in.