Why we stopped generating whole apps in one shot
The default shape of an AI app builder is a single enormous guess. You describe an app, the model writes every file it can think of, and you get a wall of code back. Sometimes it runs. Often something in the middle is broken, and now you're debugging generated code you didn't write, in a codebase you've never read, with no idea which of the forty files is at fault.
We shipped that for a long time. The failure mode that finally made us change it wasn't dramatic — it was quiet. A build would come back looking complete, the preview would render, and the app would be subtly dead: a button that did nothing, a form that saved to memory and lost everything on refresh. The generation looked like a success. You only found out it wasn't by using it.
One big guess is a bad bet at any model quality
The intuitive fix is a better model. We tried that too, and it helps — but it doesn't change the underlying maths.
A one-shot generation has to get every part of the app right simultaneously: the data layer, the state, the routing, the styling, the wiring between them. Each part has some chance of being wrong. Those chances compound. A model that's 97% reliable per component is only about 74% reliable across ten of them — and modern apps have more than ten moving parts.
Worse, one-shot generation has no feedback loop. The model never finds out whether the thing it wrote actually runs. It emits the whole app, the process ends, and any error is discovered by you, later, out of context.
Plan first, then build one feature at a time
What we do now: before writing any code, the builder plans the app as an ordered list of features. Foundation first — the shell, the data model, the primary screen — then each capability on top of it.
Then it builds them one at a time. Not one file at a time; one feature. "Add a task" is a feature. It touches whatever it needs to touch, and it either works or it doesn't.
After each one, the app is running in a live preview in your browser — an actual dev server, not a mockup — and the builder tests the feature it just wrote. It clicks the button. It types into the field. It checks that the row appeared.
When a check fails, that feature gets fixed and re-checked before anything else is built on top of it. This is the part that matters most, and it's the part one-shot generation structurally cannot do: errors get caught while they're still one feature wide. In a monolithic build, a broken data layer poisons every screen above it and you find out at the end. Here it fails on feature one, gets fixed, and feature two is built on something that works.
Verifying that it rendered is not verifying that it works
This was our own hard-won lesson, and it's worth being blunt about because we got it wrong first.
Our early checks confirmed the UI updated. Type a task, see the task appear, green tick. That check passes on an app that stores everything in a JavaScript array and forgets it on reload — which is exactly the app nobody wants, and exactly what an optimistic UI produces when the write never reaches the database.
So the checks got stricter. When a feature is supposed to persist something, the builder now writes a value with a random token appended, then asks the backend for the data again and looks for that specific token. The row cannot exist unless the write genuinely landed. A green tick means the data is really there.
The same principle applies to errors. A feature isn't verified if the app rendered while the console was throwing, or while an API call was failing in the background. Those now fail the check rather than passing quietly.
You get the app back before it's finished
After the foundation is built and verified, the run stops and hands you the app.
This is deliberate. It's the moment where a misunderstanding is cheapest to fix — the data model exists, the main screen works, and nothing has been built on top of the assumptions yet. If the shape is wrong, say so, and the plan gets revised before the remaining features are written against it. If it's right, continue.
The alternative — discovering the misunderstanding after everything's built on it — means throwing away work that was correctly implemented against a wrong premise.
The honest tradeoff
Feature-by-feature is slower and it costs more. Several generations instead of one, plus a check after each, plus fixes when checks fail. There is no version of this that is cheaper than one big guess.
What you're buying is that the app works when it arrives. For a throwaway experiment that isn't worth much. For something you intend to ship, the debugging you skip is worth considerably more than the extra minutes.
It's also why it's a paid feature, with one free build on every account: the honest way to sell it is to let you watch it happen once, and decide for yourself whether the difference is real.
Shipd turns a prompt into a complete, multi-page app — and reads your codebase so the output matches it. Free to start, no credit card.
Start building free