From sentence to working mobile app: building AppX in 90 days

Origin story — what AppX's first prototype was, the three architectures we tore down to get here, and the lesson we kept relearning: the AI doesn't need more intelligence, it needs more structure around its outputs.

AppX team ·

From sentence to working mobile app: building AppX in 90 days

AppX is an AI mobile app builder. A user types a sentence — "make me a habit tracker with streaks" — and a few seconds later their phone is holding a running app inside Expo Go, generated from that sentence. From the first prototype to live-for-users took about 90 days. This is the honest log of what that build looked like — which architectures we tried first, what failed, and what stuck.

Three months sounds short. It only felt short in retrospect. From the inside it was three architectures stacked on top of each other, each one replacing the previous, each one teaching the same lesson in a different accent.

Version 1: web-first (and why we threw it away)

The first version of AppX did not generate mobile apps. It generated web apps.

The pitch was clean: user types a sentence, the LLM emits a React + Tailwind project, we push it to a static host, the user sees their app at a URL within ten seconds. We had it working in a weekend. The demo video was great. The screenshots looked like a real product.

Then we put it in front of people who had asked us for an app builder, and the same question came back every time: "Cool, but can I see it on my phone? Like, actually on my phone?" Not in a mobile-viewport simulator. Not as a responsive web layout. On their phone, in their hand, behaving like an app.

We had built the wrong thing. Or more precisely: we had built the thing that was technically easier and called it the product. Web is easier. Web has Vercel-style static hosts, predictable bundling, every LLM under the sun has read a million React tutorials. Mobile is harder. Native modules, Expo, Metro, a custom bundling protocol, an actual device on the other end.

We spent the first month of AppX shipping the easy version and discovering, in slow motion, that the hard version was the only version that mattered. We pivoted to React Native plus Expo Go inside that first month. Most of the code we had written got thrown away. Some of the prompt engineering survived. Most of the architectural decisions did not.

The lesson: a product demo that looks like the product is not the product. The mobile-on-the-phone bit was the entire moat. Everything we did before we accepted that was throat-clearing.

Version 2: single-shot generation (and where it broke)

The second version of AppX generated React Native instead of web. It worked the same way the first version had: one big LLM call, one big prompt, all the files come out the other end. We called it "single-shot generation" because we wanted to believe it was that simple.

It was, at two screens.

At three screens, it usually worked. At four screens, the model started forgetting things. By five screens, it was collapsing in the same way every time. The theme tokens in theme.ts would say primary: '#4F46E5', but the screen the model was writing in the same response would reference colors.brand instead. A route registered in App.tsx would not match the route name the screen tried to navigate to. A type imported in one file was misspelled in another. The model knew what it was supposed to be doing in any individual file. It could not hold the cross-file invariants in its head across a long generation.

We tried bigger prompts. We tried better instructions. We tried examples. We tried telling the model, in capital letters, multiple times, what cross-file consistency meant. Some of those things helped marginally. None of them fixed the underlying problem, which is that asking a single forward pass to keep coherent state across a dozen files is the wrong shape of work.

So we split the loop in two. Plan, then generate.

Version 3: plan-then-generate

The architect plans. The generator generates. They are different prompts, different model calls, sometimes different model tiers.

The architect's only job is to write a plan: which files exist, what each one is for, which screens reference which routes, which theme tokens are canonical, which types are shared. The plan is small. It fits in a single context. The architect does not write any actual code — it writes the contract that the code has to satisfy.

Then we hand the plan to a generation pass that writes the files. Critically, the generator does not invent its own filenames, its own routes, its own theme tokens — it pulls them from the plan. The plan is the source of truth. When the generator gets confused about whether the button color is colors.primary or colors.brand, it looks at the plan instead of guessing.

This worked. Five screens worked. Eight screens worked. The class of bug where two files disagree about a shared name mostly went away, not because we wrote a smarter prompt but because we removed the situation where the model had to remember a shared name across files in a single pass.

The architecture lesson here is the one we kept relearning: the AI does not need more intelligence, it needs more structure around its outputs.

The edit engine

Generation got the user to a first app. The next thing every user wanted was to change it. "Make the button red." "Add a settings screen." "Move the streak counter to the top."

Our first instinct — the instinct we had to actively unlearn — was to re-run generation. Take the new instruction, throw away the old code, generate a fresh app with the modification baked in. It worked. It also lost the user's previous edits, because every regeneration is a fresh roll of the dice.

The edit engine was the third architectural rewrite. It reads the existing code, plans a surgical diff, and applies it via a SEARCH/REPLACE protocol against the actual files on disk. The architect produces a plan like before. The editor then walks file by file, finding the exact lines to replace, and emits a patch — not a rewrite.

A bunch of things had to be true for this to work safely. We snapshot before every edit, so a bad patch is reversible. We type-check the result, so an edit that breaks the project's TypeScript loudly fails instead of silently shipping broken code to the user's phone. We enforce a read-before-edit rule, so the editor cannot patch a file it has not actually loaded — no editing from imagination. When a SEARCH block doesn't match, the architect re-plans with the failure as context rather than retrying blindly.

None of this is glamorous. None of it is "make the model smarter." All of it is scaffolding the loop the LLM lives inside.

What we'd tell our day-1 selves

Build the structure first. Drop the LLM into it second.

In retrospect, almost every painful pivot we made in those 90 days was the same shape: we had assumed the LLM was the load-bearing part of the system, and we kept being wrong about that. The LLM is a component. The interesting work is the loop around the LLM.

The loop is: a typed plan that the model has to fill in. A validator that checks the model's output against invariants before anyone sees it. A snapshot that lets us undo when the model is wrong. A second model pass that catches what the first one missed. A retry policy with bounded recovery instead of infinite tool-calling. A clean separation between "plan" (what should happen) and "execute" (do the thing).

Type-checking the plan beats hoping the model gets it right. Validating cross-file invariants beats retrying. Snapshotting before edits beats prompt engineering for safety. Decomposing a hard task into structured sub-tasks beats one heroic prompt.

If we had known on day one that the work was 80% scaffolding and 20% prompt engineering, we would have built less throwaway code in the first month. We would not have generated web apps before we generated mobile ones. We would not have tried to one-shot five screens before splitting into plan-then-generate. The lessons were always going to teach themselves; we just paid for tuition in the form of code we deleted.

Closing

AppX exists because someone can type a sentence and get an app on their phone. That sentence sounds like a marketing line. It is also, almost exactly, the architectural North Star — every pivot we made in those 90 days was about narrowing the gap between "user typed a sentence" and "user is holding a working app," and removing failure modes along the way.

The hardest part of the build was not making the model smart. The model is already smart enough. The hardest part was building the kind of structure around the model where smart is sufficient — where the model doing its best is also the product working correctly.

Three architectures in three months. The fourth one is being written right now. We will probably throw most of it away too. That seems to be how this kind of work goes.


Try your own app idea

Describe your app in AppX →