How RapidNative Handles Asset Management in Generated Apps
By Suraj Ahmed
27th Jun 2026
Last updated: 27th Jun 2026
When you drag an image into a RapidNative chat and the AI immediately starts writing a <Image source={require("@/assets/logo.png")} /> into your screen, a surprising amount of infrastructure is doing work behind the scenes. The file has to land in object storage, get a path the Metro bundler can resolve, get linked to the right project, get surfaced to the language model as context, and eventually get bundled into the ZIP you download when you ship the app.
RapidNative is an AI-native mobile app builder that turns plain English (and sketches, screenshots, and PRDs) into real React Native and Expo apps. Asset management sounds boring — until you realize it's where most AI code generators silently break. Models hallucinate filenames. Metro chokes on dynamic require() calls. Static <Image> URIs get hardcoded and bricked when the source goes down. So we built a pipeline that takes asset references out of the model's hands and into deterministic infrastructure.
This post is a tour of that pipeline — from upload to export — for anyone curious about how an AI mobile app builder actually moves bytes around.
Photo by Rami Al-zayat on Unsplash
Why Asset Management Is the Hard Part of AI Code Generation
Generating React Native code from a prompt is the demo-friendly part of the problem. Generating React Native code that still works when you press "export" three days later is the boring, hard part. Asset references are the worst offenders.
A model that's free to invent paths will happily write:
<Image source={require("./images/logo.png")} />
…when the file is actually at assets/logo-abc123.png. The next bundler run fails. Worse, dynamic requires — require(filename) with a variable — are a hard error in the Metro bundler because asset resolution happens at build time, not runtime. The React Native team is explicit about this in the official Images docs: the path argument has to be a static literal so it can be statically analyzed.
The fix isn't smarter prompting. The fix is making the model's job impossible to get wrong: feed it the exact filename, force a single canonical path alias, and treat asset binaries as a separate class of files that the bundler can find on its own. That's what the pipeline does.
The Upload Path: From Drag-and-Drop to Object Storage
The user-facing entry point is the dropzone inside the editor. A global drag-and-drop listener in ReactDropZone.tsx catches any image being dragged over the window, validates the MIME type (.jpeg, .jpg, .png, .gif), and dispatches Redux actions to pass the file into the chat composer.
When the user sends the message, the file gets posted as FormData to POST /api/upload. The route handler does five things in order:
- Authenticate the user via NextAuth session.
- Sanitize the filename — strip special characters, kill path traversal attempts.
- Deduplicate — if the same filename already exists for the project, append a 6-character nanoid suffix (so
logo.pngbecomeslogo-7gh3kq.png). - Upload to Supabase Storage at
{projectId}/fs/{targetDir}/{fileName}(the default target isassets). - Register the asset in two tables.
That double-write to the database is the architectural choice that makes the rest of the system work. The first table, project_assets, is the asset gallery — schema below — and exists so we can list, count, and clean up everything a project owns. The second table, files, is the bundler's source of truth for what code and binaries belong to a project. The asset gets a row in files with is_external: true, which is the flag that tells the export pipeline: "don't try to read this as text; download the bytes from Storage."
Why Supabase Storage and not Vercel Blob or S3 directly? Two reasons. First, RapidNative's projects, users, and team data already live in Supabase, so RLS policies can govern asset access without a second auth boundary. Second, Supabase Storage exposes a CDN-backed public URL pattern (/storage/v1/object/public/projects/...) that you can shove straight into a React Native Image uri for previews without signing anything.
The Database Schema
There are two tables involved. Here's project_assets, introduced in the migration 20250903093026_create_project_assets_table.sql:
CREATE TABLE public.project_assets (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
kv_project_id TEXT NULL,
project_id UUID NOT NULL
REFERENCES public.projects(id) ON DELETE CASCADE,
url TEXT NOT NULL,
uploaded_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
A later migration (20260319120000_add_asset_type_to_project_assets.sql) added an asset_type column that defaults to 'image' but supports 'app_icon' for the AI-generated icons we'll get to in a moment. It also adds a composite index on (project_id, asset_type) so the "show me the three app icon candidates for this project" query stays fast.
The files table is bigger and lives behind 20250820131402_create_project_files_table.sql:
CREATE TABLE public.files (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
project_id UUID NOT NULL
REFERENCES public.projects(id) ON DELETE CASCADE,
file_path VARCHAR(500) NOT NULL,
mime_type VARCHAR(100),
file_type file_type,
encoding file_encoding,
content TEXT NOT NULL,
file_size INTEGER,
is_external BOOLEAN NOT NULL DEFAULT FALSE,
...
);
The is_external column was added later (20260316100959_add_is_external_to_files.sql) — same migration that creates the project-assets public Storage bucket and the read RLS policy. Before this, every binary asset had to be base64-encoded into the content text column, which worked for tiny logos but exploded for anything over a megabyte. The external-flag pattern is the moment Storage and Postgres start collaborating cleanly: Postgres tracks the path and metadata, Storage holds the bytes.
The cascading deletes do real work. When a project is deleted, ON DELETE CASCADE pulls both project_assets and files rows out automatically. Storage cleanup runs in a separate worker so a slow Storage delete can't block a project teardown.
Photo by Mark Decile on Unsplash
How Generated Code Actually References Assets
This is where the AI generation pipeline earns its keep. When the model writes a screen that references a user-uploaded image, it doesn't get to pick the path. The system prompt forces a single pattern:
<Image
source={require("@/assets/logo-abc123.png")}
style={{ width: 100, height: 100 }}
/>
The @/ alias is set up in the project's rapidnative.json (similar in spirit to tsconfig.json path aliases) and maps to the project root, so @/assets always means the project's assets/ directory. That alias is honored at bundle time by Metro and at IDE time by TypeScript, which means the same path string is valid in three different layers — the language model, the editor, and Metro — without anyone needing to coordinate.
The system prompt is also strict about the inverse case. From the actual generation rules (lightly paraphrased):
User-uploaded assets:
source={require("@/assets/filename.png")}with the exact filename from context.Image URLs live ONLY in seed data. Every
<Image source={{ uri: ... }} />in a screen must read its URI from a query-result variable —item.image_url,user.avatar_url. Never hardcode a URL string in JSX.
The reason for that second rule is the same reason we don't trust the model to invent filenames. If a model hardcodes https://i.imgur.com/some-id.png directly into JSX, that string survives forever — through edits, exports, App Store submissions. When the host eventually rots, the app rots with it. Forcing URLs through the seed/query layer means there's a single, observable, mockable place where images come from.
Inside the AI generation route (src/app/api/user/ai/generate-v2/), the asset context passed to the model includes the exact upload result — filename, MIME type, and an instruction. The model never sees the storage URL because the model doesn't need it; only the bundler does.
Icons and Fonts: Why It's Lucide All the Way Down
A clean asset story falls apart the moment a developer types "an icon for the settings screen." Icons are the part of every React Native app that everyone forgets is also assets — fonts, SVGs, glyph maps. RapidNative made a deliberate, almost ruthless choice here: there is one icon library. It's lucide-react-native, version ^0.510.0, and the generation rules forbid anything else.
The generated import pattern looks like this:
import { HomeIcon, SearchIcon, UserIcon } from 'lucide-react-native';
import { cssInterop } from 'nativewind';
cssInterop(HomeIcon, {
className: {
target: 'style',
nativeStyleToProp: { color: true }
}
});
<HomeIcon size={24} className="text-primary" />
Every icon gets the Icon suffix and a cssInterop registration so NativeWind can drive the icon's stroke color from a Tailwind class. There's also an approved list of Lucide icons baked into the system prompt — partly to prevent runtime crashes from import paths that don't exist in this version, partly so the model can't summon, say, a "Cardano" icon nobody has heard of and break the bundle.
Custom fonts get the same treatment. The Expo template scaffold ships with a font loading setup through expo-font, but user-uploadable custom fonts aren't part of the supported asset pipeline. The choice keeps the surface area small and rules out an entire class of "my app renders blank on cold start because the font hasn't loaded yet" bugs.
App Icon Generation: Three Variants, One Pick
App icons are the most user-visible asset in any mobile app, and they're the asset most people don't have when they start. So RapidNative auto-generates three on the first message of a new project.
The service that runs this lives at src/modules/api/services/ai/IconGeneratorService.ts. On first prompt, it dispatches three parallel generations through FAL AI — specifically the fal-ai/flux/schnell endpoint, which runs Flux Schnell at 4 inference steps and returns in a few seconds. The prompts are three different style descriptions: "Flat & Modern," "Gradient & Glossy," and "3D Rendered."
Each result gets downloaded server-side, uploaded into Supabase Storage under {projectId}/fs/assets/app-icon-{style-slug}-{nanoid}.png, and inserted into project_assets with asset_type: 'app_icon'. The user sees all three in the editor and picks one. The PUT endpoint at /api/user/projects/[projectId]/app-icon then:
- Writes the chosen icon to
assets/images/icon.png(the path the Expo template'sapp.jsonreferences). - Deletes the other two from Storage with
storage.remove(BUCKET, [paths]). - Deletes the
project_assetsrows forasset_type: 'app_icon'so the gallery doesn't keep ghosts.
Icon generation is gated behind a feature flag (APP_ICON_GENERATION_ENABLED) in dev so local development doesn't burn FAL credits on every test prompt. It runs by default in production.
Photo by Andrew M on Unsplash
Image-to-App: Assets That Become Apps
There's a second mode of "asset," and it's the inverse of the upload flow. When a user feeds a screenshot or wireframe into Image to App, the image is both:
- Vision input to the language model — Claude's vision mode reads pixel-level layout to extract structure.
- A project asset that the generated screen often
require()s directly so the new screen's hero image is the same one the user uploaded.
Inside generate-v2, the presence of an imageUrl parameter switches the model purpose to 'VISION', which loads a different tool set tuned for layout extraction. The system prompt also instructs the model to treat the image as a saved asset: "Saved as project asset. Use: require('@/assets/{imageFileName}') in Image components."
It's a small trick, but it's the difference between "the model talks about your screenshot" and "your screenshot becomes the first screen of your app."
The Export Pipeline: Bytes to a ZIP
The endpoint that handles export is POST /api/user/projects/[projectId]/download. It does the harder half of asset management — taking a project that's been living in Postgres and Storage and turning it into a ZIP that runs on any Mac with Expo CLI.
The pipeline:
- Fetch all files for the project from
files. Skiptasks.json(internal-only state). - For each file, branch on
is_external:- If
is_external: false, the content is in the row — UTF-8 text, add it to the ZIP atfile_path. - If
is_external: true, download the binary from Supabase Storage withsupabase.storage.from('projects').download(...), then add the bytes to the ZIP at the same path.
- If
- Stream the ZIP built with
adm-zipto the client. - Post-process for one specific migration — older projects used
react-native-linear-gradient's old import format; the export rewrites those references on the fly so a freshly downloaded project always uses the current scaffold.
Export is gated behind a paid plan. Free-tier users can build and preview in the editor; only Pro and above can pull the ZIP. The plan check lives in teamSaasPlanRepository. The motivation isn't punitive — it's that ZIP export is when RapidNative stops being a hosted product and starts being a code generator, and that's the value boundary.
Trade-offs and What's Missing
There's no image optimization layer. Uploaded images go into Storage at their original resolution and format, and the React Native Image component does the resizing at runtime. For most use cases (logos, screen-sized hero images) this is fine. For a photo-heavy app it's not — there's no automatic AVIF or WebP conversion, no responsive @2x/@3x variant generation, no Cloudinary-style URL transforms.
There's no visual asset library in the editor either. You can drag in new assets, and the AI can reference assets it knows about, but there isn't a "browse all the images this project owns" panel. Asset management is implicit — through the chat, the generated code, and the exported ZIP — rather than explicit through a UI.
Storage cleanup on project deletion is handled by a worker rather than a cascading trigger. This is a deliberate choice — Storage operations can be slow, and you don't want a 30-second teardown blocking a user clicking "delete project."
These are all things you'd call out in a code review. They're also all things that exist because the pipeline as it stands is doing the load-bearing work, and the optimizations would only matter at a different scale.
Why It Matters for AI-Generated Apps
The pattern you take away from this isn't specific to RapidNative. Any AI code generator has to draw the same line: which decisions does the model make, and which decisions does deterministic infrastructure make?
For asset references, RapidNative's answer is "the model makes none." The model gets shown a filename. The model is forbidden from inventing paths. The model can't choose icon libraries or generate URLs. Every binary file is tracked twice — once for the bundler, once for the user-facing gallery — and the export pipeline reconciles both into a ZIP. The result is generated code that survives the round trip from chat → Supabase → Metro → App Store with the same confidence as code a human wrote.
If you're building anything that turns natural language into shippable React Native apps, that boundary — between what the LLM decides and what the system decides — is the boundary worth thinking hardest about. Assets are just where it shows up first.
Try It
If you want to see this happen end-to-end, start a project on RapidNative, drag an image into the first chat, and watch the generated screen reference it by name. Then download the ZIP, open assets/, and see your file sitting next to the AI-picked app icon. The whole pipeline — upload, storage, generation, export — is what makes "drag in an image" feel like nothing but does, under the hood, a lot.
If you're more curious about the broader product, the pricing page lays out where ZIP export sits, or you can browse other RapidNative engineering posts for more under-the-hood writeups.
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.