Why we picked Expo Go for AI-generated apps (and rejected native builds)
Native builds = 60s+ per iteration kills the AI feedback loop. Web preview defeats the mobile-app pitch. Expo Go + Metro gives sub-second hot-reload on a real phone — and that latency budget cascaded into every architectural decision downstream.
AppX team ·
An AI agent editing a mobile app makes ten to fifty small changes in a session. The user types "make the button bigger," watches it happen, types "now make it teal," watches that happen, types "actually move it to the bottom." If each of those edits costs sixty seconds of rebuild time, the user puts their phone down somewhere around edit three. By edit five the phone is back in a pocket. By edit seven they have decided the product is broken and closed the tab.
If each edit costs a hundred milliseconds, the phone never leaves their hand. They type the next request while still looking at the result of the last one. The product feels alive.
The entire decision about how AppX shows users their generated app comes down to that math. We had three real options. Only one of them put a hundred-millisecond preview on a real phone, and that constraint dictated almost every other piece of infrastructure we built afterward.
What we evaluated
| Option | Rebuild per edit | Verdict |
|---|---|---|
| Native iOS / Android | 60–120s | Authentic, but iteration-killing |
| React Native Web | ~1s | Fast — but "it's a website" defeats the mobile pitch |
| EAS Build | 5–15 min | Right for shipping, death for the chat loop |
| Expo Go + Metro | ~100ms | The only option that keeps the phone in your hand |
Three paths, each with a different shape of latency.
Native iOS and Android builds. Xcode for iOS, Gradle for Android, deploy to a real device over USB or wireless debug. This is the "real" answer — it's exactly what the user would build if they were a developer. Each rebuild is sixty to a hundred and twenty seconds. Authentic, slow, iteration-killing. You can build a beautiful app this way. You cannot iterate on one in chat.
Web preview via React Native Web. Render the generated React Native code in a browser using react-native-web. Under a second to refresh. We prototyped this first because it was the easiest thing to ship. Users hated it. They had come to a mobile-app builder and we were showing them a website. The complaint was unanimous: "but does it work on a phone?" If the preview is in a browser, the pitch collapses — it becomes a no-code web tool with extra steps.
EAS Build. Expo's cloud build service produces a real installable native binary. Five to fifteen minutes per build. Excellent for shipping to the App Store. Death for the chat loop. We knew immediately this was the wrong tool for the inner loop, but the right tool for the outer one — we still use EAS for production builds, just not for iteration.
Expo Go plus Metro. The user installs Expo Go on their phone once, scans a QR code once, and lives inside that preview for the rest of the session. Metro — the React Native bundler — serves JavaScript bundles to the phone over a websocket. When the code changes, Metro pushes a new bundle in roughly a hundred milliseconds. The user never re-scans the QR. The phone never sleeps. The app just changes.
That last one was the only option that kept the user's attention on the phone instead of on the spinner.
The latency budget reasoning
There's an old rule of thumb that we mostly trust. Under two hundred milliseconds feels instant. Two hundred milliseconds to a second feels responsive. One to five seconds feels broken. Beyond five seconds users disengage and switch context — they check Twitter, refill their coffee, forget what they were doing.
Map those tiers to the options and the answer is obvious.
- Expo Go + Metro: 100ms — instant tier.
- React Native Web: ~1s — responsive tier, but defeats the product pitch.
- Native builds: 60-120s — past the disengage tier entirely.
- EAS Build: 5-15min — somewhere between "make a coffee" and "go for a walk."
Only one option lived in the tier where the user keeps their eyes on the phone during the edit. Every other consideration — authenticity, production parity, native-module support — was secondary to whether the iteration loop felt alive.
What this choice cascaded into
Picking Expo Go was a single decision that quietly forced about ten others.
Metro is a stateful long-running process. It holds an in-memory dependency graph, watches the filesystem, and serves bundles over a custom protocol. You don't restart it per edit — you push files into the directory it's watching and let it diff. That meant every user needed their own warm Metro process. A user, a project, a container, a Metro — one of each, alive for the duration of the session.
A warm container per user meant routing. The phone scans a QR code that points at a specific hostname; that hostname has to resolve to whichever container is currently warm for that user. We needed dynamic routes from {project}.preview.appx.uz into the right place, updated as containers come and go.
A warm container per user also meant the first user of the day couldn't wait sixty seconds for Docker to pull an image, install dependencies, and boot Metro. We needed containers pre-warmed before the user arrived — a pool, sized to recent demand, refilled in the background.
And pushing edits in meant the backend had to write source files into the container's watched directory and let Metro do its incremental thing. Not "rebuild the app." Just "drop a file in, watch the bundler re-bundle that one file."
That stack — the pool of pre-warmed containers, the per-project routing, the file-push pipeline — is most of what Forge does. The Expo Go decision was upstream of all of it. If we'd picked native builds, we wouldn't have a warm pool, we'd have a build queue. If we'd picked web preview, we wouldn't have a container per user, we'd have a CDN. Different choice, different product, different company.
What we gave up
This is a preview surface, not a release artifact. The thing the user sees on their phone is running inside Expo Go's harness — it isn't a standalone app, it can't be submitted to the App Store, and uninstalling Expo Go takes the preview with it. We added EAS Build as a separate path for users who want a real installable binary at the end of their session. Two pipelines: one for iteration, one for shipping.
We're scoped to the Expo SDK. Anything that requires a native module outside the SDK — a custom-compiled native dependency, a third-party library that ships native code — doesn't work in Expo Go. We've kept the AI's available API surface scoped to the Expo SDK explicitly, so the generated code never references something that "works" in the codebase but fails when the phone loads it.
And we don't get easy OS-specific behavior. Expo Go runs on both iOS and Android, which is great, but if a user wants deep platform integration — Live Activities, iOS widgets, Android intents — those are an EAS Build away, not a chat edit away.
Why this is right for builders, not publishers
The frame that resolves all of this is: the user is iterating, not publishing.
The inner loop of a builder product is "make a change, see the change, decide if the change is good, make another change." Latency in that loop compounds. Every second you add is multiplied by the number of edits in a session, and every edit the user doesn't make is a question they don't get to answer about what their app should be. Iteration latency is the moat.
Production builds matter exactly once per app — at the end. They are a separate path, on a separate timeline, with separate tradeoffs. Putting EAS Build in the chat loop would be like putting git push in the autocomplete loop. Right tool, wrong moment.
Closing
If we were building a publishing tool — "describe an app, get a binary, ship to the store" — we would have picked native builds and accepted the queue. The product would be slower, but the artifact would be authentic from the first generation.
We are not building a publishing tool. We are building an iteration tool that ends in a publish. The inner loop is the product. Everything we picked — Expo Go for preview, Metro for hot-reload, a warm container per user, a pool of pre-provisioned containers, a routing layer keeping the QR code pointed at the right place — exists to keep the inner loop under a second.
The user's phone is in their hand. The point of the architecture is to keep it there.