Inside Our AI Pipeline: From Prompt to React Native Component Tree

(155 chars):

RI

By Rishav

22nd Aug 2026

Last updated: 22nd Aug 2026

Inside Our AI Pipeline: From Prompt to React Native Component Tree

You type "build a fitness tracker with a home screen, a workout log, and a stats page." Ninety seconds later, an iOS-shaped preview on your right shows three real screens, the tab bar highlights the one you're on, and a fake workout you just added appears in the list. No build, no reload, no npx expo start. That gap — between a sentence and a mounted component tree — is where the entire product lives. This post walks the whole AI code generation pipeline from the keystroke that submits a prompt to the moment React.createElement returns something the preview can render.

There is no magic in that gap. There are seven stages, each with a specific job, most of them non-obvious. What follows is the actual architecture: real file paths, real dependency versions, and the reasoning behind the parts that took the longest to get right.

Mobile app being built on a laptop screen Photo by Andrew Neel on Unsplash

Why a Pipeline at All?

You could, in principle, wire a browser text box to an LLM, take whatever comes out, and eval it. People do this in demos. It survives about one prompt.

Production code generation for real mobile apps has failure modes that a naive "prompt → text → render" loop simply cannot handle:

  • A streaming LLM emits half-closed JSX while tokens are still arriving. Try to parse that with Babel and you get a syntax error every 40 ms.
  • A model that reads no context hallucinates importsButton from a component library you don't have installed, hooks that don't exist on this Expo version.
  • A model producing a new file at /app/settings/notifications.tsx writes it into the VFS, but the running app has no idea that route exists — the module graph was baked when the bundle loaded.
  • An app.json rewrite mid-stream can trigger repeated full reloads that destroy partial render state.

Each of those is a real bug we've fixed. Each fix is a stage in the pipeline. The rest of this post explains what each stage does and why it exists.

Stage 1: The Prompt Hits the API

When you press Enter in the chat, a Redux thunk in src/modules/editor/ dispatches a POST to /api/user/ai/generate-v2/route.ts. The payload is small: { query, projectId, imageUrl?, streamId }. Everything else — conversation history, project files, template selection — is fetched server-side from Supabase, because the client isn't authoritative about any of it.

The route immediately does two things that most tutorials skip:

  1. Provider routing via AIModelConfigService. RapidNative doesn't hardcode a model. A DB-backed config, cached 5 minutes, tells the route which provider and model to use for this purpose ("main-generation", "context-gathering", "quick-edit"). The AI SDK adapters live in package.json under @ai-sdk/anthropic@^1.2.12, @ai-sdk/openai, @ai-sdk/deepseek@^2.0.44, @ai-sdk/amazon-bedrock, @ai-sdk/google-vertex, and @openrouter/ai-sdk-provider@0.7.5. Anthropic, OpenAI, DeepSeek, Bedrock, Vertex, xAI, and OpenRouter are all live and pickable per-task.
  2. Resilient SSE session initialization. We assign the response a streamId and register it with a resilientSSE.resume() handler. When the user's laptop hits a bad Wi-Fi patch mid-generation, the client reconnects with the last event ID and picks up exactly where it dropped. No duplicate tool calls, no state corruption. Details of the streaming approach are covered in how we stream AI-generated code in real time.

A heartbeat (startSseHeartbeat()) fires empty comment lines every few seconds so the browser doesn't kill an idle connection during long inference on a slow model.

Stage 2: Context Gathering (First LLM Call, With Tools)

Here is the single most consequential architectural decision in the pipeline: we do not give the code-generation model tools. The context-gathering model does, and it runs first, in a separate call.

The reasoning is empirical. Give one LLM both "read files as needed" tools and "write code" instructions, and it degrades after three or four tool calls. Attention spreads. Token budgets bloat with tool-call/tool-result JSON. It starts skipping context it should have read, or reading the same file twice.

So /api/user/ai/generate-v2/route.ts runs a first pass with a smaller, cheaper model exposing this tool set (from the fullstack template's ToolsProvider):

  • get_files_content(paths, lineRange?) — read specific files
  • list_dir(path) — enumerate what's in the project
  • glob(pattern) — find files by pattern
  • batch_grep(patterns[]) — search contents
  • get_images_by_keywords(keywords) — fetch stock imagery for UI mockups
  • For fullstack-supabase projects: list_skills, search_skills, read_skills

maxSteps is capped at 10. When the loop ends — either the model has enough context or it hit the ceiling — we take the concatenated tool results and pass them as a system message to the main model in stage 3.

The prompt for this stage is getContextGatheringPrompt(), and it's narrow: figure out what files matter for the user's request, read them, done. It does not attempt to write code.

Developer looking at code on multiple monitors Photo by Christopher Gower on Unsplash

Stage 3: Generation (Second LLM Call, Streaming, No Tools)

Now the real work. streamText() from Vercel's AI SDK v4.3.19 is invoked with the main model — usually a large Claude or GPT — and a system prompt assembled by getTemplateSystemPrompt() in src/modules/api/services/ai/config/prompt-builder.ts.

That prompt is not a single string. It's composed from PromptSection objects — ROLE_SECTION, RESPONSIBILITIES_SECTION, MOBILE_NATIVE_SECTION, UNSUPPORTED_TAILWIND_SECTION, and template-specific sections — each with id, priority, enabled, overridable, and content fields. The builder sorts by priority and drops sections a template disables. Different templates (fullstack-v2, fullstack-supabase, nativewind-themed) compose different final prompts.

The constraints baked into MOBILE_NATIVE_SECTION and UNSUPPORTED_TAILWIND_SECTION are hard-won:

  • React Native, not web. <div> is a bug.
  • NativeWind + Tailwind 3.4.17+ only. grid is unsupported — horizontal grids use flex-row flex-wrap with computed basis-%.
  • SafeAreaView from react-native-safe-area-context@^5.6.2 is required. Tabs specify edges={['top', 'left', 'right']} explicitly to avoid double bottom padding.
  • No useSearchParams / useLocalSearchParams in dynamic route defaults — a subtle bug that hallucinated undefined at first render before dispatching real routing.

The model streams a mix of block types via fullStream: text (conversational messages to the user), codeproject (fenced code with file paths), action (things like "install this package"), tool-call, tool-result. Blocks are delimited so the client can start rendering the chat message, the file list, and the preview independently as they arrive.

Tokens arrive at roughly 40-80/sec depending on provider. Nothing waits for the full generation. This is why the perceived latency feels closer to a chat than to a build.

Stage 4: Streaming Assembly (Blocks → Files)

On the client, a stream reader in the editor thunks pulls SSE events off the wire and hands them to transformMessageContent(). This function's job is unglamorous but critical: turn a stream of partial text into distinct blocks, and each codeproject block into a set of { filePath, content } entries.

The parser handles a fun edge case: the model is often mid-token when a re-render happens. If we naively parse the current buffer, we get a JSX file with an unclosed tag. The jsx-fixer from src/shared/utils/jsxFixer.ts (also mirrored in src/lib/coding-agent/jsx/jsxFixer.ts) handles this — but crucially, we don't run it on every keystroke. We wait for the block to complete, then run the post-processor pipeline once.

For each completed file, the thunk dispatches setFiles() into editorSlice, which mirrors the VFS in Redux so the rest of the UI (file tree, code editor, error panel) can react to it. This mirror is important — it means "the AI is writing to home.tsx" produces UI in the file tree and the code editor before the preview iframe even sees the change.

Stage 5: The Post-Processor Pipeline

Streaming is done. We have a bag of files with plausibly valid content. Before writing them to the VFS, src/shared/utils/postProcessor.ts runs a pipeline of checks and fixes:

  1. content-cleaner — trim junk whitespace and stray markdown fences
  2. package-json-guard — never let the model corrupt package.json; only allow additions to dependencies, and only ones we've seen before or can resolve
  3. duplicate-var-fixer — deduplicate const foo = ... when the model repeated a declaration
  4. jsx-fixer — a custom AST walker (not a full Babel parse — too slow, too brittle on partial input) that closes unclosed tags, disambiguates TypeScript generics like <T> from JSX like <Component>, and handles structural elements (function bodies, return statements, conditional blocks)
  5. import-dedup — remove duplicate imports the model produced when patching a file it had already partially imported from
  6. import-fixer — for symbols the file uses but doesn't import, look up the source via JSX_IMPORT_MAP and add the import (this is how a model can "forget" to import Text from react-native and still get working code)
  7. schema-validator — for fullstack-supabase, validate that SQL migrations parse against the current schema in pg-mem before writing them
  8. syntax-validator — a final acorn@^8.16.0 parse to catch anything the earlier stages missed

Each processor runs in try/catch. A processor that throws is logged and skipped — never allowed to stop the pipeline. The philosophy is: AI output is untrusted input, and the post-processor pipeline is the boundary between untrusted text and code we're willing to hand to the bundler.

This design is not defensive coding — it's a direct response to a class of bugs that used to ship broken previews on maybe 5% of generations. Post-processing dropped that failure rate low enough that we could remove the "hit retry" button we used to show.

Team collaborating around a laptop Photo by Annie Spratt on Unsplash

Stage 6: VFS Write + Route Registration

Now the files land somewhere real. RapidNative's preview runs entirely in the browser via a sandboxed environment powered by @lifo-sh/core@0.10.9. The sandbox exposes an in-memory virtual file system — sandbox.kernel.vfs — which is what the AI actually writes into.

const vfs = instance.sandbox.kernel.vfs;
vfs.write('/app/home.tsx', content);

That single write triggers a file watcher inside the sandbox that a metro bundler (running as a long-lived worker via browser-metro@^1.0.35) is subscribed to. Standard HMR-style updates flow from there. Deeper background on this layer lives in how we built instant React Native preview without a build step.

But writing a file isn't enough for a new route. Expo Router uses a file-system convention: /app/home.tsx becomes a route /home. Under normal expo start, that mapping is generated by a Metro plugin scanning the filesystem. In-browser, browser-metro doesn't know how to do that scan.

So we synthesize the plugin's output ourselves. A helper called buildExpoRouteContext(vfs) walks the VFS, finds every app/**/*.tsx, and writes a synthetic module at __expo_ctx.js:

// __expo_ctx.js — auto-generated, rewritten on every add/remove of a route
var modules = {};
modules["./home.tsx"] = require("./app/home");
modules["./workouts.tsx"] = require("./app/workouts");
modules["./stats.tsx"] = require("./app/stats");
// ... plus a `ctx` shim mimicking Metro's require.context
module.exports = ctx;

expo-router reads this module to build its route tree. When the AI adds a new file, we rewrite __expo_ctx.js and — because no running module in the bundle has an HMR accept boundary for it — a single controlled full reload picks up the new route. Existing routes hot-swap normally.

There is a very sharp corner here we hit repeatedly during development: do not hot-update __expo_ctx.js. HMR silently succeeds ("hot update (0 modules)") because nothing imported it explicitly, and the running app keeps its stale route table. One deliberate document reload after a route add is correct behavior. Repeated mid-stream reloads are destructive — they discard partial render, and measured, they made the preview show nothing at all.

Stage 7: Metro Bundling and the Iframe Mount

The preview iframe (SandboxPreview in src/modules/studio/components/custom/editor/) is not just an <iframe src="...">. It mounts PreviewBrowser from @lifo-sh/ui@^0.10.3, which is a chrome-like shell that talks to the sandbox through a service worker bridge at /_sw/<boxId>/<port>/. That bridge proxies HTTP requests from the iframe (on port 8081, where metro serves) into the sandbox's port registry.

Concretely: the iframe requests /index.bundle?platform=web → the SW bridge routes to the metro instance inside the sandbox → metro reads the VFS, bundles what's needed, streams a response back. First bundle typically returns in 800-2000ms; incremental HMR updates in 50-200ms.

SandboxPreview does one thing that took a long time to get right: it waits for __expo_ctx.js to be registered before mounting. Attempting to mount an iframe against a metro that hasn't seen the new route yet produces expo-router's "Unmatched Route" screen, which is a dead end — the fix is a full reload, but by then the user has already seen the failure. A PREVIEW_FAILED_MS = 120_000 watchdog surfaces a real error state if a bundle genuinely never arrives.

Runtime errors and console logs flow back the other way via postMessage, land in Redux at editorSlice.runtimeErrors, and either surface in the UI or feed the next AI turn as diagnostic context. This is how "there's a red screen in the preview" becomes "the AI fixes the red screen in the next prompt" without the user copy-pasting anything.

What This Looks Like End-to-End

Putting the seven stages together, one prompt travels:

User keystroke → Redux thunk → /api/user/ai/generate-v2 → provider routing (AIModelConfigService) → Stage 2: context-gathering LLM with 6 tools, capped at 10 steps → tool results → Stage 3: main LLM streams fullStream via SSE → Stage 4: client parses blocks into files → Stage 5: post-processor pipeline (8 stages) → Stage 6: VFS write + __expo_ctx.js regenerate → Stage 7: metro rebuild → HMR (or single reload for new routes) → mounted component tree in the iframe.

Perceived latency: first text token ~1-2s, first file appearing ~5-10s, mounted preview ~15-30s for a fresh project. Follow-up edits ("make the button green") often land in 3-5s end-to-end because context-gathering has the answer cached and generation is small.

Why This Shape?

Three questions this architecture answers that a simpler one doesn't:

Why two LLM calls? Because one LLM given both context-gathering tools and code-generation instructions gets worse output than two LLMs each doing one job. The empirical difference is large enough to justify the extra call.

Why so much post-processing? Because streaming LLM output is inherently mid-state — half-closed JSX, forgotten imports, mid-token generics. The post-processor pipeline is the boundary between untrusted probabilistic text and something we're willing to give to a bundler. See more on how we prevent AI code hallucinations.

Why an in-browser metro at all? Because a server-side build would add 5-15 seconds of latency per iteration, cost real compute per user, and give no interactive preview at all. The VFS + browser-metro + service-worker-bridge design lets the preview update in under 200 milliseconds after a file write. This is the difference between "AI code generator" and "AI mobile app builder" — you feel the app while it's being written.

Frequently Asked Questions

Which LLM providers does the pipeline support? The provider is configured per task via AIModelConfigService and cached from a database. Adapters are installed for Anthropic Claude, OpenAI (via Azure), DeepSeek, Amazon Bedrock, Google Vertex, xAI, and OpenRouter's multi-provider routing. Any of them can be selected as the model for context gathering, main generation, or quick edits.

How does the preview update without a build step? Generated files are written to an in-memory virtual file system (sandbox.kernel.vfs) exposed by a browser-based sandbox. A metro bundler runs inside the sandbox and watches the VFS. When a file changes, metro rebuilds affected modules and streams an HMR update to the iframe through a service-worker HTTP bridge, typically in 50-200ms.

What prevents the AI from writing broken code? A post-processor pipeline runs before any file reaches the VFS: content cleaning, package.json guarding, duplicate-var fixing, JSX-fixer (closes unclosed tags), import dedup, import-fixer (adds missing imports via JSX_IMPORT_MAP), schema validation (for SQL migrations), and a final acorn parse. Each processor is isolated in try/catch so one failure never blocks the others.

How do new routes appear without a full app restart? A synthetic __expo_ctx.js module is regenerated whenever routes change, giving expo-router the same map it would get from Metro's require.context at build time. Existing routes hot-swap normally; adding a new route triggers exactly one controlled document reload — never repeated mid-stream reloads, which are destructive.

Try It

The best way to understand a pipeline is to watch it run against your own prompt. Start a project on RapidNative — 20 free credits, no card — and describe an app in one sentence. Watch the chat, watch the file tree fill in, watch the iframe mount. Every stage above is happening. You can also start from a sketch on the whiteboard or a PRD — different inputs, same pipeline downstream.

If you're curious about the SEO-adjacent parts of the stack, we've also written about the multi-LLM router, the prompt architecture, and the virtual file system that ties them together.

Start now

Ready to build your app?

Turn your idea into a production-ready React Native app in minutes.

Free tools to get you started

Questions

Frequently 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.