Inside the Export Pipeline: From AI Generated React Native Code to the App Store
By Rishav
2nd Jun 2026
Last updated: 2nd Jun 2026
Most AI app builders sell you a black box. You type a prompt, you watch a phone-shaped preview light up, you pay for a subscription — and somewhere in the middle, the actual code your app is made of becomes someone else's problem.
That works until you need to ship. The App Store has opinions about your app.json. TestFlight wants a signed .ipa. Google Play wants a .aab with a real applicationId. And if any of that breaks, "type another prompt" is not a debugging strategy.
RapidNative is built around a different assumption: the AI generated React Native code is yours, end-to-end, every keystroke. This post walks through what that means architecturally — the four stages a generated file passes through on its way from a streamed LLM token to a published mobile app, and the deliberate design choices behind each stage.
The pipeline keeps the same code visible from prompt to App Store — no transformation layers hiding behind a SaaS. Photo by Tudor Baciu on Unsplash
The Four Stages, in 60 Seconds
Every file in a RapidNative project moves through four stages:
- Generation — an LLM streams TypeScript directly into the editor's in-memory virtual filesystem.
- Preview — a Web Worker running a metro-compatible bundler turns those files into a live JS bundle, rendered in an iframe with Fast Refresh.
- Persistence — files are written to a Supabase
filestable; binary assets are pushed to Supabase Storage. - Handoff — the project is exported as a real Expo workspace, either as a ZIP (
adm-zip) or as a fresh GitHub repository generated from a template. From there, the user runseas buildand submits to the stores themselves.
The hand-off step is where most AI builders quietly fail. RapidNative makes it the whole point.
Stage 1: Generation — The LLM Doesn't See Magic, Just Files
The AI generation endpoint lives at src/app/api/user/ai/generate-v2/route.ts. It is built on the Vercel AI SDK (ai@^4.3.19) and uses a two-step prompting architecture: a cheap model runs first with tool access (file reads, search, image inspection) to gather context, then the main model — Claude or one of several configurable providers — generates code with no tools, just text streaming back to the client.
The important point for the pipeline is what the LLM is targeting. It is not generating a custom DSL or a JSON tree that gets compiled to React Native later. It is generating real .tsx files that conform to Expo Router's file-based routing conventions: app/_layout.tsx, app/index.tsx, app/(tabs)/profile.tsx. Each file is a normal React Native component using normal React Native primitives (View, Text, Pressable) and a real styling system (NativeWind, gluestack, or StyleSheet, depending on the project's component_base column in the projects table).
Why this matters: the file the LLM writes is the file you ship. There is no intermediate representation that needs to be re-rendered to "real" code at export time. Generation and export operate on the same artifact.
As tokens stream in, the editor's Redux store updates a per-file content map. A debounced sync pushes changes to /api/user/projects/[projectId]/files (POST/PATCH), and the bundler worker is notified of the change set so the preview can update without waiting for the network round trip.
Stage 2: Preview — A Real Bundler in the Browser
This is the single most contentious architectural decision in the pipeline, so it gets the most space.
The naïve approach to live-previewing React Native code in a web app is to hand it to Expo Snack and let snack.expo.dev handle bundling and previewing in a remote sandbox. That works, but it introduces a round trip per change, a service dependency, and a code path that diverges from what the user will actually run when they export.
RapidNative bundles in the browser. The bundler lives at src/modules/file/bundler.worker.ts, runs in a Web Worker (so it cannot block the editor's main thread), and is built on browser-metro@^1.0.19 — a metro-compatible bundler with IncrementalBundler, VirtualFS, and a TypeScript transformer that can run entirely client-side.
The worker speaks a small message protocol:
// main thread → worker
{ type: 'watch-start', files, packageServerUrl }
{ type: 'watch-update', changes }
{ type: 'watch-stop' }
// worker → main thread
{ type: 'watch-ready', code, stubbedFiles? }
{ type: 'watch-rebuild', code, stubbedFiles? }
{ type: 'hmr-update', update, bundle }
{ type: 'error', message }
The transformer stack is TypeScript-to-JS plus React Fast Refresh instrumentation, parsed by acorn with @sveltejs/acorn-typescript's JSX-enabled TS plugin. The bundle is rendered inside an iframe with an HTML scaffold (src/modules/file/bundle-html.ts) that injects shims for nativewind, expo-font, react-native-reanimated, react-native-webview, and a synthetic expo-constants — all the bits of a native runtime that don't exist in a browser but are touched by typical Expo libraries.
The most clever part is error containment. When a single file fails to compile (a stray brace, a half-written import after a streamed token), the transformer doesn't crash the whole bundle. It replaces the broken module with a BrokenComponentStub and records the error in a stubbedFilesInBundle map. Every other screen keeps rendering. The editor reads the stub map after each rebuild and decorates the broken file with the formatted error, including a code frame.
Bundling in the browser means the preview is fed by the same source tree the export will ship. No server round-trip, no Snack divergence. Photo by James Harrison on Unsplash
The architectural payoff of in-browser bundling is consistency. The bundle that powers the live preview is built from the same files, with the same TypeScript transformer, that get serialized to disk during export. There is no "preview-only" code path that ships subtly different output than the export.
Stage 3: Persistence — Files in Postgres, Bytes in Object Storage
Code lives in a Supabase files table. The schema (supabase/migrations/20250820131402_create_project_files_table.sql) is intentionally boring:
| Column | Type | Notes |
|---|---|---|
id | UUID | primary key |
project_id | UUID | foreign key to projects |
kv_project_id | TEXT | legacy URL slug, unique with file_path |
file_path | VARCHAR(500) | e.g. app/index.tsx, package.json |
mime_type | VARCHAR(100) | e.g. text/typescript-jsx |
file_type | ENUM | typescript, tsx, javascript, etc. |
encoding | ENUM | utf-8, ascii, binary, base64 |
content | TEXT | the file body, when small enough |
file_size | INTEGER | bytes |
is_external | BOOLEAN | if true, content lives in Storage |
created_at / updated_at | TIMESTAMP |
The is_external flag (added in 20260316100959_add_is_external_to_files.sql) is the design decision that lets the system store both .tsx source and arbitrary image assets without bloating Postgres. Text files live in the content column. Binary assets — uploaded screenshots, logos, AI-generated images — are uploaded to the project-assets Storage bucket at projects/{projectId}/fs/{filePath} and the row in files becomes a pointer with is_external: true and a null or summary content.
The persistence APIs reflect this split:
GET /api/user/projects/[projectId]/filesreturns the merged set, fetching binary content from Storage only when explicitly requested.POST /api/user/projects/[projectId]/files/batchupserts many files at once — used heavily during AI generation, when the model emits multiple files in one streamed turn.POST /api/user/projects/[projectId]/files/renamedoes the path-uniqueness check that database constraints can't express cleanly.
Two things to notice. First, the schema treats app/index.tsx and package.json identically — Postgres doesn't know which one is a React component and which is a manifest. That's the right call: the export pipeline reassembles a real workspace from these rows without needing a schema-level concept of "the entry point."
Second, the schema is what a developer would design, not what an AI-tooling vendor would. There's no proprietary intermediate format. A SQL SELECT file_path, content FROM files WHERE project_id = ? reconstructs the project. That's the same property that makes the export step trivially correct.
Stage 4a: Export as a ZIP — A Real Expo Workspace
The download endpoint at src/app/api/user/projects/[projectId]/download/route.ts does what you'd expect, but the details matter.
It uses adm-zip@^0.5.16 for archive construction. (jszip@^3.10.1 is in package.json from earlier iterations and is no longer used — a small piece of historical debt that does not affect the pipeline.) The flow is:
- Authenticate via NextAuth, resolve the user's email to a
users.id. - Verify ownership: the requesting user is either the project's
user_idor an admin (isEmailAdmin(userEmail)). - Check subscription via the new
teamSaasPlanRepository— exports are gated for free-tier accounts, which is a product decision, not a technical one. - Pull all rows from
filesfor the project. - For any row with
is_external: true, fetch the binary from Supabase Storage atprojects/{projectId}/fs/{filePath}and add it to the ZIP with its original bytes. - Apply lightweight code migrations to
.js/.tsfiles — currentlymigrateLinearGradientfromsrc/modules/editor/utils/migrateLinearGradient— to upgrade legacy patterns to current Expo API surface. - Generate a
.envfile with database connection strings and user-defined environment variables viagetProjectEnv(). - Stream the ZIP back as
{projectName}-expo.zip.
What's in the archive is a complete Expo workspace:
my-app-expo/
├── package.json # real npm deps with real versions
├── app.json # Expo config (name, slug, ios.bundleIdentifier, android.package)
├── eas.json # EAS Build profiles
├── tsconfig.json
├── .env
├── app/ # Expo Router screens
│ ├── _layout.tsx
│ └── index.tsx
├── src/
├── components/
└── assets/
The versions inside that workspace are the ones that get audited at submission time: Expo SDK 54.0.13, React Native 0.81.4, expo-router 6.0.12, React 19.1.0. These come from the project templates in tools/project-templates/ — RapidNative ships three (fullstack, nativewind-themed, admin-dashboard) and the template a project starts on is recorded in the component_base column.
After unzipping, the path to a running build is the standard Expo path: npm install, npx expo prebuild if you want native source folders, eas build --platform ios --profile preview. There is no proprietary glue.
Stage 4b: Export to GitHub — The Template Generation Pattern
Some users want a Git repository, not a ZIP. The /api/user/projects/[projectId]/init-git/route.ts endpoint produces one. The pattern is worth calling out because it's different from the obvious approach.
A naïve implementation would use @octokit/rest (it's actually in the website's devDependencies as @octokit/rest@^19.0.0, so the team has it on the shelf) to clone the template, walk the files, and force-push to a new repository. RapidNative does not do this.
Instead, it calls the GitHub REST API directly:
const TEMPLATE_REPO = 'RapidNative/expo-template';
await fetch(
`https://api.github.com/repos/${TEMPLATE_REPO}/generate`,
{
method: 'POST',
headers: { Authorization: `token ${GITHUB_TOKEN_FOR_PROJECTS}` },
body: JSON.stringify({
owner: GITHUB_USER_FOR_PROJECTS, // 'rapidnative-dev' or 'rapidnative-production'
name: projectId,
private: true,
}),
}
);
GitHub's Generate from template endpoint creates a new repo with the template's contents in a single API call, server-side, with no local clone. RapidNative then commits the project-specific files on top.
The win is throughput. A 100-file Expo workspace would otherwise require ~100 sequential Contents API writes, rate-limited at 5,000 requests/hour. Template generation costs one. The trade-off is that the template repo itself becomes a piece of infrastructure that has to be kept in sync with the export templates — a one-time fixed cost in exchange for permanently fast repo init.
GitHub's template-generation endpoint makes repo provisioning a single API call. Photo by Hack Capital on Unsplash
Stage 4c: EAS Build and the App Store — Why RapidNative Stops Here
This is the question that comes up in almost every architecture review of the pipeline: why doesn't RapidNative have a "publish to App Store" button that runs eas build and eas submit server-side?
There is zero EAS or App Store integration in the codebase. The templates ship an eas.json and the npm scripts you'd expect (eas build, eas submit, eas update), but no API route invokes them. By design.
Three reasons:
Apple's submission process is stateful and identity-bound. Submitting an .ipa requires an Apple Developer account, a signing identity, provisioning profiles, App Store Connect API keys, and a real, human-bound App ID. An AI builder that puts your binary into the store under its own developer account creates a fragile coupling that survives until you cancel your subscription and discover your app's identity belongs to a vendor. Letting the user run eas submit from their own machine, against their own Apple developer account, is the only configuration that survives that test.
EAS Build is itself a remote build farm. Wrapping a remote build farm inside another remote build farm to save a cd && eas build is mostly an exercise in re-implementing build queues. The user's own EAS account gets the build logs, the cache, the priority queue, and the dashboard.
The export is the product boundary. The cleanest architectural surface between RapidNative and the App Store is the ZIP. Everything below the ZIP is "your code, your account, your build." Everything above it is "AI generation, preview, collaboration, billing." Crossing the line would mean owning App Store credentials, payment relationships, and policy compliance — a different product.
If you want to see the seam, the Expo docs for EAS Build and EAS Submit describe exactly what RapidNative deliberately doesn't wrap.
What "No Lock-In" Means at the Architecture Level
The phrase "no vendor lock-in" gets thrown around so much it's lost most of its meaning. Architecturally, in this pipeline, it cashes out as five concrete properties:
- The LLM's output is the file on disk. No transformation step rewrites generated code into a vendor format.
- The preview bundler runs the same transformer as a normal Metro build. The browser preview is not a special render path.
- Persistence is
SELECT file_path, content FROM files. Anyone with read access to the database can reconstruct the project. - Export is a standard Expo workspace.
npx expo startafternpm installis sufficient. - Build and submit run on user-owned accounts. RapidNative holds no Apple or Google identity on behalf of the user.
The result is that "switch off RapidNative tomorrow" looks like git clone $YOUR_REPO && cd $YOUR_REPO && npm install && eas build. Nothing breaks. The AI builder was scaffolding, not load-bearing.
That property is the one most AI builders cannot ship, because their preview pipeline, their generation format, or their App Store integration introduces a coupling that the export can't unmake. The pipeline above is, end to end, the answer to "how do you build an AI mobile app builder that doesn't quietly hold your code hostage?"
Try It
If you want to see this pipeline run, the easiest entry point is to start a project at rapidnative.com and watch the preview tick as code streams in. When you're ready, hit the Download button and unzip the result — it's a normal Expo workspace you can eas build from your laptop, or gh repo create and push wherever you like.
Related reading on adjacent layers of the system: the streaming AI code generation architecture, the two-step LLM pipeline that produces the code, how production-grade state management gets generated, and the practical follow-up — how to publish a React Native app to the App Store once you have the ZIP in hand. Pricing details and credit costs are on the pricing page.
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
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.