How We Built RapidNative's Editor: A Technical Deep Dive
By Riya
12th Sep 2026
Last updated: 12th Sep 2026
Describe an app in plain English. Watch a working React Native screen render in the browser two seconds later. Click a button in that preview, ask for it to be blue, and see the file — the real .tsx source — update on disk.
That last sentence hides a lot of engineering. There is no "backend that generates code and returns it." There is a streaming tool-calling loop, a Redux store with roughly eighty fields, an in-browser Linux sandbox running a real Metro bundler, a service worker translating iframe traffic, a virtual file system that the AI reads and writes like a filesystem, a second cloud runtime for the phone preview, and a SQL-first database engine that runs actual Postgres inside a WASM binary. All of that has to stay in sync while text is still streaming in.
This is a technical walkthrough of how the AI mobile app builder that powers RapidNative actually works — the modules, the data flow, and the trade-offs we've made along the way.
A modern editor is one visible layer on top of many invisible ones — Photo by Ilya Pavlov on Unsplash
The system, in one paragraph
The editor is a Next.js 15 App Router client that talks to five things at once: a Redux Toolkit store holding all local editor state, a Server-Sent-Events stream from an AI generation route that returns tool calls in real time, an in-browser sandbox (@lifo-sh/core) that runs the user's app and re-bundles on file changes, a Supabase Postgres database that persists project files, and a separate cloud service ("orchd") that runs the same app on real infrastructure so a phone can scan a QR code and see it. When a user types a prompt, all five systems participate — and the interesting engineering is in keeping them coherent when the AI is still writing and the user has already clicked to move on.
Why an in-browser bundler at all?
The single biggest architectural decision behind the RapidNative editor is that the preview is not a screenshot, a mock, or a server-rendered snapshot. It is the actual React Native app running inside your browser tab. That decision — made because "iterate on a live app" is fundamentally a better product than "wait 30 seconds for a rebuild" — dictated everything else.
To run a React Native app in a browser you need three things: a JavaScript runtime (the browser already has one), Metro (React Native's bundler, which normally runs as a Node process), and a file system for Metro to watch. We get Metro-in-browser from a package called browser-metro running inside a WASM Linux sandbox provided by @lifo-sh/core (version 0.10.15). The sandbox exposes a virtual file system, a shell, and long-lived processes. We seed its /app directory with the project files, start Metro, and let it watch. Every file write becomes an HMR event; every dependency change becomes a full bundle reload.
The wiring lives in src/modules/file/lifo-sandbox.ts. When a file changes in Redux, we push it into the sandbox's VFS. When Metro emits a hot update, the preview iframe reacts. The iframe communicates with the outer editor through a postMessage bridge — the sandbox's HTTP requests are routed through a service worker under /_sw/<boxId>/<port>/, which is the trick that lets a browser tab act like a networked machine.
There is a second preview surface that most technical readers miss on their first pass: the phone. When you scan the QR code in the editor, you are not connecting to the browser sandbox — you are connecting to a workload running on our cloud infrastructure ("orchd"), provisioned per project. That workload runs rnrun (Expo dev server) and serves its bundle over public URLs. Keeping those two runtimes in agreement is one of the harder problems in the codebase, and we have a whole internal skill dedicated to it.
The Redux store: one place all state lives
The editor is a single-page React app driven by Redux Toolkit (@reduxjs/toolkit 2.8.2). We resisted moving away from Redux several times during rewrites; it kept winning. The reason is boring: when there are eight subsystems changing state and any of them can fire while the AI is streaming, one predictable reducer beats eight cleverly-coordinated hooks.
The store is defined in src/modules/editor/store/store.ts and combines five slices under a single root reducer:
editor— the canvas. Artboards (dictionary by id), files (dictionary by path), selected layer, selected file, world dimensions, zoom, error state, AI request-in-progress flags, chat messages.app— team context, credit balance (broken into free / paid / topup with their own expiry windows), subscription tier, the loaded agent config.themeEditor— color-picker state for the theme customization mode.recovery— checkpoint data for undo and rollback.baseApi— RTK Query cache for remote data.
The store's boot sequence in store.ts also spins up four singletons: a MessageManager (the postMessage RPC bridge to the preview iframe), a TimelineEventLog (the debug recorder), a RecoveryOrchestrator (abort handling and retry logic), and a DogWatcher (a watchdog that trips if the AI request flag stays true too long). All four are initialized outside React, because they need to survive component remounts and the streaming pipeline can't be paused while the tree suspends.
A concrete example of why single-store discipline matters: credit accounting. When the AI charges credits, the reducer optimistically deducts before the server confirms, then reconciles five seconds later against the source of truth. If a second AI request lands during that window, the reducer sees a consistent local number. When the network refetch returns a stale value (which happens more than you'd expect), a dedicated reconcile step decides which value wins. That entire policy lives in appSlice.ts and is invisible from the outside — which is exactly what "state management done right" feels like.
Every editor decision compresses into a state-management decision — Photo by Emile Perron on Unsplash
The AI pipeline: streaming tool calls, not a code blob
If you have looked at first-generation "prompt to code" tools, you know the pattern: send a prompt, wait, receive a big lump of source, replace files. We do not do that.
The generation route lives at src/app/api/user/ai/generate-v3/route.ts and uses Vercel's AI SDK v6 (aliased in package.json as ai6 — we run v4 for a few legacy paths and v6 for the current agent). It executes a tool-calling loop: the model is given a set of tools (read_file, write_file, list_files, read_schema, create_migration, search_skills, ask_question, fetch_page, and a few more) and streams its work — thinking, tool calls, tool results, and text — back to the client as a sequence of UIMessage parts.
The client renders those parts live. When the model calls write_file, we dispatch a Redux action to update the file in the store; the sandbox picks it up; Metro rebuilds; the iframe reloads. All of that happens while the model is still deciding what to do next.
Three details matter here:
Prefix caching. The system prompt plus pre-loaded skill documentation is identical across turns — big and stable enough that the LLM providers (DeepSeek, Anthropic, Google Vertex, OpenRouter, xAI) can cache it. First-message and follow-up prompts diverge deliberately: the first message loads first-message-specific skills; follow-ups get the leaner "core skills" bundle, saving roughly six thousand tokens per turn. Reasoning blocks from prior assistant turns are stripped before resend, because leaving them in changes the cache key and quietly turns cache hits into cache misses.
Streaming resume. Every generation is given a streamId. If the network drops or the tab reloads, the client reconnects with streamId + lastEventId and the route resumes the same stream — no duplicate charge, no lost partial file. This is important because generations are long enough (thirty seconds to a few minutes) that flaky connections would otherwise cost users real money.
Storage abstraction. The AI tools do not talk to Supabase directly. They talk to an interface (FileStorage) with three implementations: MemoryFileStorage for the streaming route, LifoFileStorage for the sandbox VFS, and FsFileStorage for a Node harness (npm run agent:run) that lets us drive the same agent from the command line against a real directory. That interface is the single reason we can test the agent as executable behavior instead of hoping.
Point-and-edit: how a click becomes a code change
One of the loved features of the editor is the ability to click any element in the preview and describe how to change it. Under the hood this is a two-way channel between the iframe and the outer editor, hinged on a custom DOM attribute called data-bx-path.
During Metro's build we inject an attribute onto every rendered JSX element that encodes its source location as filepath:line:column. A bridge script inside the iframe listens for clicks, walks up the DOM to find the nearest element with data-bx-path, and posts the result out to the editor. The MessageManager receives that message and dispatches a selectLayer thunk that updates Redux with the file, line, and column.
Two footguns are worth mentioning because they cost us real bugs. First: data-bx-path values are metro-root relative (app/(app)/index.tsx), while artboard keys elsewhere in the app are workspace-root relative (mobile/app/(app)/index.tsx). Comparisons must be suffix-wise and in both directions — a straight equality check silently drops selections. Second: rn-preview-reload events must carry the artboard-key form (no leading slash); the sandbox filters with an exact compare and drops anything else without warning.
For quick edits like "make this blue" we bypass a full round-trip through the AI: we read the selected line, run Prettier in the browser to parse the JSX, mutate the relevant prop, and write back. That path is capped in scope — anything the local parser can't confidently fix falls back to the AI agent — but it makes trivial edits feel instant.
Click-to-edit works because every DOM node knows exactly which line of source produced it — Photo by Kelly Sikkema on Unsplash
The SQL-first data layer
Fullstack apps generated by RapidNative use Supabase. But the editor also needs to answer questions about the schema before a real Supabase database exists (during the streaming generation), and it needs to validate that a proposed migration will actually apply.
For that we embed Postgres. Not a "mostly Postgres" subset — real Postgres, compiled to WebAssembly, running in a background worker. The library is @electric-sql/pglite (0.5.4). Every project keeps its schema as ordered SQL files in supabase/migrations/, and every AI create_migration tool call is validated by applying the SQL on top of the current schema in PGlite before the file is written. The tool returns success only if PGlite accepted the SQL.
Why this matters: a validator more permissive than the runtime is worse than no validator. We learned this the hard way. An earlier version used @tinbase/pg-mem — a Postgres subset — as the validation engine. A user project came through whose first migration declared profiles.id as text and compared it to auth.uid() (which returns uuid) in an RLS policy. pg-mem accepted the comparison; real Postgres rejected it at CREATE POLICY. The whole migration chain aborted at runtime with no tables and no data. The db_migration_new tool had reported success. Every screen was empty.
We switched the engine to PGlite, added an "engine fidelity" test that pins the behavior, and shipped a schema linter that reports RLS-enabled tables without any policy as errors (silent empty tables are the single worst failure mode for a builder like this). The AI now sees those lints as feedback and fixes them mid-generation.
The generated mobile/src/db/types.ts file is regenerated on every migration so client code stays typed against reality. And the mobile app talks to Supabase through a plain @supabase/supabase-js client, not a custom adapter — one fewer moving part to reason about.
The two-preview problem, and how we keep it honest
A file created by the AI must appear in both previews: the browser sandbox (editor) and the cloud workload (phone). Files that live only in the sandbox never reach the phone. Files pushed only to the workload never appear in the editor. And the AI's list_files tool must see the same reality the user does — otherwise it happily writes on top of files it can't see.
The rule we settled on is that project files are the single source of truth. They live in the project_files table in Supabase, they seed the sandbox VFS, they are synced to the orchd workload's /app, and they are what the AI reads. Anything you want to show on device but hide from the editor and the AI has to not be a project file at all. The pre-generation "Building your app…" loading screen is a good example: it lives on the mobile workload only, pushed there at provisioning. It never leaks into the editor, and the AI never sees it.
The reason we insist on this is that the alternative — filtering — is impossible to enforce. Our fullstack agent has a shell tool (bash) and uses ls, cat, and grep to enumerate the codebase. There is no single chokepoint. Any file the storage layer knows about is a file the AI will find.
Real-time feedback: HMR, watchdogs, and the debug bar
Streaming a code generation while a user is watching creates a class of bugs that are effectively invisible without the right instrumentation. A screen doesn't render. The preview reloads too often. The iframe lands on Expo Router's "Unmatched Route." These failures cross four layers — file write, Metro rebuild, HMR broadcast, iframe render — and a stack trace from any single one tells you nothing.
We built three tools for this class of bug.
window.__rnLog is an always-on timeline recorder in dev builds. It captures Metro stdout, HMR events, every rn-preview-reload decision (including scheduled-but-never-fired ones, which are otherwise invisible), and 500ms change-only snapshots of iframe URLs and visible text. __rnLog.download() writes it out as JSON. Every hard bug in the streaming pipeline over the last three months was located by opening that JSON.
window.__rnDiag() is a read-only one-shot snapshot: route context, VFS route files with sizes, artboards, iframe state. Useful mid-stream to answer the question "is the screen even in the module graph, or is the frame on a different route?"
/project/blank is a dev-only harness that mounts the real project route with no database, auth, or AI attached. It replays a captured generation verbatim at its measured cadence. The rule that makes it worth anything: it reuses project/[projectSlug] deliberately. Every time we've been tempted to build a standalone reproduction page, the standalone page has stopped reproducing the bug. If the harness passes and a real project fails, the defect is not in the write-to-render path — that inference has located five separate bugs.
The DogWatcher completes the picture. It monitors the isAiRequestInProgress flag. If a generation runs past its threshold without emitting new tool calls, the watchdog fires recovery — the same code path a manual "stop" button would run — and hands control back to the user. Without it, a hung provider would leave the UI locked with no visible failure.
Trade-offs we made (and would make again)
Redux over Zustand/Jotai. For an app this asynchronous, one place all state lives and one place all mutations happen is worth more than the ceremony. RTK Query lives in the same store, so remote state doesn't create a parallel universe.
In-browser Metro over server rebuilds. Server rebuilds would be simpler. They would also make every iteration a network round trip and turn "fast" into "unusably slow." The complexity we accept for browser bundling pays for itself the moment a user makes their second edit.
Streaming tool calls over batch codegen. Batch codegen would return more predictable diffs. Streaming makes the product feel alive: the user sees the model deciding, changing its mind, reading files before writing them. It is also the only way to justify long-running generations from a UX standpoint.
SQL migrations over ORM abstractions. ORMs would be nicer to generate. SQL is what the AI understands, what Supabase enforces, and what PGlite validates. Any layer in between would be a translation the AI would fight.
Two previews, one truth. We could have unified the browser and cloud runtimes. Instead we made "the project files are the source of truth" a hard invariant and let each surface do its own thing. Simpler mental model, harder to get wrong.
Every architectural line in the editor was drawn to keep the AI, the user, and the runtime looking at the same truth — Photo by Annie Spratt on Unsplash
What this all costs
Roughly: Next.js 15.3.6, React 18.2, Redux Toolkit 2.8.2, Supabase 2.57.4, NextAuth 4.24.11, Vercel AI SDK v6 (ai6 6.0.197) for the agent path, PGlite 0.5.4 for schema validation, @lifo-sh/core 0.10.15 for the browser sandbox, browser-metro 1.0.36 as the bundler, Monaco Editor 4.7 for the code view, Tailwind 4 for the UI, and Inngest 4.2.4 for background work.
The stack looks conventional from the outside. Most of what makes it work is not visible in package.json — it is in how the pieces are wired, where the invariants live, and which questions the abstractions refuse to answer.
Try the editor
If you'd rather see this than read about it, open the editor, type a prompt, and watch. Twenty free credits, no card. If you want to see the same pipeline handle a design instead of text, try the image-to-app path or PRD-to-app for a longer-form input.
For the AI side specifically, our earlier post Inside RapidNative: How AI Turns a Chat Prompt into Production React Native Code covers the model routing and prompt engineering in more depth. For the click-to-edit mechanism, Visual Editor for React Native: How Point-and-Edit Works Under the Hood picks up where this post left off. And if you want the trade-off comparison against traditional agencies, Mobile App Development Cost: Agency vs AI Builder is the practical version.
React Native itself is documented at reactnative.dev, Expo at docs.expo.dev, the Vercel AI SDK at ai-sdk.dev, and PGlite at pglite.dev. Everything else is us.
Ready to build your app?
Turn your idea into a production-ready React Native app in minutes.
Free tools to get you started
Free AI PRD Generator
Generate a professional product requirements document in seconds. Describe your product idea and get a complete, structured PRD instantly.
Try it freeFree AI App Name Generator
Generate unique, brandable app name ideas with AI. Get creative name suggestions with taglines, brand colors, and monogram previews.
Try it freeFree AI App Icon Generator
Generate beautiful, professional app icons with AI. Describe your app and get multiple icon variations in different styles, ready for App Store and Google Play.
Try it freeFrequently asked questions
What is RapidNative?
RapidNative is an AI-powered mobile app builder. Describe the app you want in plain English and RapidNative generates real, production-ready React Native screens you can preview, edit, and publish to the App Store or Google Play.
Can I export the code?
Yes. RapidNative generates clean React Native and Expo code that you can export at any time. No lock-in, no proprietary format. Hand it to your developers or keep building inside RapidNative.
Is RapidNative free to use?
Yes. You can build apps on the free plan with no credit card required. Paid plans unlock unlimited AI generations, code export, and direct publishing to the App Store and Google Play.
Do I need to know how to code?
No. Most users build apps by describing what they want in plain English. Developers can drop into the code whenever they want more control, but coding is optional.
How long does it take to build an app?
Most users have a working first screen in under a minute. A full MVP usually takes a few hours instead of the weeks or months traditional development requires.