How RapidNative Handles Image and Icon Generation for AI-Built Apps
By Riya
14th Oct 2026
Last updated: 14th Oct 2026
When an AI generates a mobile app from a one-line prompt — "a workout tracker for runners" — the easy part is the code. React Native has well-known components, screens follow predictable patterns, and modern LLMs can scaffold a working UI in seconds. The hard part is everything that isn't code: a photograph of running shoes on the home screen, three plausible gym images on the discover tab, and an app icon that looks like a real brand instead of a generic placeholder square.
A screen full of broken images is a screen that doesn't convert. So inside RapidNative — our AI app builder that turns natural-language prompts into working React Native and Expo apps — image and icon generation is a first-class system, not an afterthought. This post walks through the architecture: how we cache generated images at two levels, how we steer image models away from their own worst habits, and how the agent tools fit together so a screen renders with real pictures the moment it finishes streaming.
Images and icons are what separate a "demo" from a "real app" — Photo by Alvaro Reyes on Unsplash
The two problems: content images and app icons
Before we built this system we had to split "images" into two distinct problems, because they fail in different ways and need completely different pipelines.
Content images are what you put inside the app: the hero photo on a home screen, the thumbnail on a card, the avatar on a profile. These need to look believable, they need to match a described subject ("a steaming cup of coffee"), and most importantly they need to be cheap — the agent might ask for twelve of them during a single generation. A new photorealistic image costs money; caching matters.
App icons are the single most brand-defining asset in a mobile app. They need to be square, full-bleed, visually simple enough to read at 48×48, and consistent with the app's purpose. There's only one per project — but the user wants to pick from a few options, so each generation produces a small set of visual variants.
Content images are a volume problem solved by caching. App icons are a quality problem solved by prompt engineering. Lumping them into a single "generate_image" tool would have made both worse. So we built two services.
The Image Bank: a two-tier cache in front of FAL Sana
The content-image system is called the Image Bank. It sits in src/modules/api/services/ai/ImageBankService.ts and does one job: given a subject like "jogging shoes" and a style like "photo", return a URL to a photograph of jogging shoes — ideally one we've generated before.
The naive version would be to call an image model every time. For FAL's Sana model at $0.001 per megapixel, that's cheap individually but adds up fast when the agent is generating a dozen images per app and five hundred apps a day. The right version is a cache. But caches on image prompts are tricky — "jogging shoes" and "running sneakers" are the same picture, and a strict string equality check will miss every synonym.
We solved this with two tiers inside Postgres, no external vector service needed.
Tier one: exact match on a normalized key. When the agent asks for an image, we strip framing words like "photo", "portrait", and "closeup" from the subject, lowercase it, and look up (subject_key, style, orientation) directly against the image_bank table. If a row exists, we return its URL and bump a hit counter. This handles the "same user, same prompt, two days later" case for free.
Tier two: fuzzy match via pg_trgm. If the exact key misses, we fall back to Postgres's trigram similarity using a GIN index. The query compares the normalized subject against every cached subject key and returns matches above a configurable threshold (default 0.6, tuned by the IMAGE_BANK_TRGM_SIMILARITY env var). "Jogging shoes" matches "running shoes" matches "trainers" — all without a vector embedding, without a separate similarity service, without a background job to maintain. One SQL query, one index, hits-per-dollar triples.
Tier three (miss): generate and store. Only when both cache tiers come back empty do we hit FAL Sana and generate a fresh image. The result gets uploaded to a public Supabase Storage bucket under {category}/{nanoid()}.{ext}, the URL and metadata go into image_bank, and the next request for a similar subject becomes a cache hit forever.
-- The two indexes doing the real work
CREATE INDEX idx_image_bank_subject_key
ON image_bank (subject_key, style, orientation);
CREATE INDEX idx_image_bank_subject_trgm
ON image_bank USING GIN (subject_key gin_trgm_ops);
The agent-facing tool is get_images, which accepts up to twelve requests in a single call and dedupes them before anything hits the database — if a screen asks for three images of the same thing, we generate once and return the same URL three times. The response tells the agent whether each image was a cache hit (tier: 'exact'), a fuzzy hit (tier: 'trigram'), or newly generated (tier: 'new'), which is useful during debugging but ignored by the model itself.
A good cache can turn a $0.03 generation into a 1ms Postgres lookup — Photo by Caspar Camille Rubin on Unsplash
Why Sana instead of Flux or DALL·E
We tested a lot of image models before settling. FAL Sana landed in default position for three reasons: cost ($0.001/MP vs Flux Schnell's $0.003/MP), speed (sub-second returns for 992×992 outputs), and surprisingly good adherence to simple subject prompts. Flux Schnell stayed in the fallback slot because it's measurably better at photorealistic complexity, and we switch to it via the IMAGE_BANK_MODEL env var when a particular style demands it.
Sana also has a specific weakness we had to work around in the icon generator, which brings us to the harder half of this system.
The Icon Generator: three styles, one tricky prompt
App icons live in IconGeneratorService and operate on completely different constraints than content images. There's no cache — every project needs its own icon. There's a quality bar — a bad icon is actively worse than no icon. And there's a prompt problem that cost us a weekend to understand: Sana cannot draw an app icon when you ask it for an "app icon."
That phrasing — natural as it sounds — makes the model generate rounded-rectangle mockups: a tiny icon floating inside a tilted phone screen, with a Dock below it and status bar above. Image models have been trained on screenshots that call themselves "app icons," so the token sequence doesn't mean what you think it means.
The fix was to never use the phrase. Internally, we prompt for a "flat vector graphic" of the symbol, full-bleed to the edge of the frame. The word "icon" never appears in the model-facing prompt. That single change took us from ~30% usable outputs to ~90%.
The three-style template
A single generation produces three icon variants in parallel, each with its own style spec:
| Style | Treatment | Example |
|---|---|---|
| Minimal | Single solid color silhouette on contrasting background. Zero internal detail, zero shading. | White dumbbell on deep blue. |
| Glyph | Inverted — bright symbol on a saturated field. | Deep blue dumbbell on bright yellow. |
| Classic | Two-tone flat, one accent tint, soft rounded shapes. | Blue dumbbell with lighter blue accent. |
The user sees all three in the picker and clicks the one that fits their brand. The two unselected icons stay in Supabase Storage — we don't garbage-collect, because the thumbnails stay valid in the chat history and users sometimes come back and change their mind.
Deriving concrete symbols
An icon prompt needs a thing — a dumbbell, a stopwatch, a kettlebell. "Fitness tracker" is not a drawable concept. So before image generation, we route the sanitized app description through DeepSeek (via the plan-model tier, roughly $0.0001 per call) with a prompt that asks for three simple physical objects. No abstract concepts, no UI mockups, no phones. The three concrete symbols feed into the three style variants.
This step is cheap, fast, and nearly invisible from the user's point of view, but it's load-bearing. Giving an image model an abstract concept like "productivity" produces floating gears, hour-glasses, and clip art. Giving it "stopwatch" produces a stopwatch.
Reading the user's theme colors
If the project already has a NativeWind theme defined in mobile/theme.ts, we extract the primary and background colors and append them to each icon prompt as a hint. CSS variables like "--primary": "24 24 27" (an RGB triple) get converted to hex, validated against the scaffold default (plain zinc on white is rejected as "not branded"), and fed into the prompt as named colors.
Why named colors? Because image models ignore hex codes. A prompt saying #b45309 draws red — the model has no idea what hex is. The actual steering comes from the English color name ("deep charcoal", "forest green"), with the hex appended as a hint for the small percentage of outputs that happen to use it. If no theme exists, we fall back to one of six hand-picked ink/paper pairs, deterministically selected by hashing the app description plus the style index so the same app always gets the same fallback palette.
Named colors steer image models. Hex codes don't — the model has no concept of what hex means — Photo by Balázs Kétyi on Unsplash
How the agent uses these services
The two services expose themselves to the AI agent as three tools, each with a narrow contract.
get_images fetches content images. The agent passes a batch of subject/style/size requests; the Image Bank returns URLs the agent can drop directly into JSX. Because every URL is already public and already hosted, nothing has to be downloaded or re-uploaded — the generated React Native code just references the Supabase Storage URL and the preview renders instantly.
ask_question({ type: 'app_icon_selector' }) is called once per app, as the final tool call after all screens are built. It triggers a background generation of three icons and then shows the picker UI. The agent must wait for the user's selection before continuing. This is the normal path for first-time generation.
platform_action({ action_type: 'regenerate_icon' }) is for when the user has already seen the picker and wants a new icon with specific guidance ("something darker, more minimal, with a coffee cup"). It generates a single icon and applies it immediately — no picker. The two icon tools are mutually exclusive within a single agent turn, because showing a picker after silently applying an icon would be confusing.
Together these three tools cover 95% of the image needs of a generated app. The remaining 5% is logos, and that's handled by a different pipeline entirely.
Logo-to-icon: the fetch_page + download_asset combo
When a user says "clone the Linear mobile app" or "make something like Notion's", they usually want the real brand. We handle that through two web tools working in sequence.
fetch_page scrapes a target URL and extracts a structured bundle: title, headings, body text, images (up to 25, each tagged with a context label like logo, hero, or icon), favicon, and — the clever bit — a prioritized color palette. We parse inline CSS and up to five external stylesheets, pull every hex and RGB value, resolve CSS variables, decode Tailwind class names like bg-emerald-600, and read the theme-color meta tag. Each color gets a score based on where it appeared: brand variables are worth ten points, interactive elements six, backgrounds three, text two, borders one. Colors within 25 units of each other in RGB space get merged. Neutral greys get filtered out. The result is a short list of brand colors that actually describe the site.
download_asset then takes the logo URL from that bundle and saves it directly as assets/images/icon.png in the project. The file goes into the Supabase-backed virtual filesystem (files table), gets flagged is_external: true, and gets synced to the device preview workload. We update app.json's expo.extra.selectedIconUrl to point at the public URL, mark defaultAppIconSet: true, and skip the icon picker entirely. The user already has a logo; we don't need to invent one.
PNG, JPEG, GIF, WebP, SVG, and ICO are allowed. The size cap is 5 MB. The timeout is 30 seconds. Anything that fails those gates returns a clean error and the agent falls back to the normal icon generator.
Deployment assets: when a single icon has to become seven
Once a user clicks "deploy to App Store," the single app icon has to become an entire visual family: an iOS icon, an Android adaptive icon with separate foreground and background layers, a 1284×2778 iPhone splash screen, a 96×96 notification icon, a favicon, a 1024×500 feature graphic, and store screenshots for both platforms. Expo and Google Play both have strict format requirements, and each asset has to feel like it belongs to the same brand.
This runs through /api/user/deployments/generate-image and uses sharp for everything that doesn't need to be newly generated.
Most of the derived assets are pure image manipulation. The adaptive foreground is the icon resized to 66% of a 1024×1024 frame with a transparent background. The adaptive background is a gradient from the icon's dominant color (extracted via sharp.stats()) to a 40%-darker variant, rendered as an SVG and rasterized back to PNG. The splash screen is the same gradient with a smaller centered icon. Notification icon and favicon are both just resizes with transparent backgrounds. All of this is instant — no AI calls, no latency, deterministic output.
Two assets need actual generation. The feature graphic (the horizontal banner on the Google Play listing) is generated fresh via Flux Schnell because none of the derivative tricks produce a 1024×500 landscape image that makes sense. The store screenshots are generated via fal-ai/flux/dev/image-to-image using the app icon as a conditioning input — this gives us a stylistically consistent "mock screenshot" without having to actually render the real app, which is handy for icon-first review.
Everything comes back as a base64 data URI so the frontend can display and download without an extra round trip.
The visual identity resolver
Before all of this runs, there's one preparatory call that locks in a consistent symbol-color-style spec. If the project has an icon already, we send it to a vision model (currently Llama 4 Scout via OpenRouter) and extract a structured description: SYMBOL: white triangle pointing up / PRIMARY: #7C3AED / ACCENT: #4F46E5 / BACKGROUND_STYLE: gradient from #7C3AED to #4F46E5. If there's no icon yet, we invent an identity with a smaller text model (Llama 3.1 8B) using the app name, description, and category.
That identity gets cached per session and reused across every image call in the deployment flow. Without it, each generated asset tends to drift stylistically — the feature graphic has a different accent than the screenshots, which have a different vibe than the splash. One identity, applied consistently, is the difference between "store listing" and "random pile of images."
A single visual identity spec steers every derived asset — Photo by Daniel Korpai on Unsplash
Things that broke along the way
A few lessons worth leaving here.
"App icon" is a trap phrase. As described above. Any image model trained on a web dataset has seen thousands of "app icon mockup" screenshots and will produce them when asked. Use "flat vector graphic" or "symbol" instead.
Hex codes are advisory, not instructive. Image models respond to English color names. Pass hex if you want, but pass the name too, and don't be surprised when the output ignores your palette anyway. Diffusion models aren't literal.
Trigram beats embeddings for short strings. We prototyped a pgvector embedding cache before shipping the trigram version. For two-to-four-word subject keys like "running shoes" or "coffee cup," pg_trgm is faster, cheaper, and nearly as accurate — and it doesn't require a separate embedding call to populate. We kept a pgvector column on the table for future use, but it's not in the hot path.
Keep unselected icons around. The user's chat scrolls backward. If they look at an older message and see three thumbnails they rejected, those thumbnails need to still work. Deleting the two "losers" from Storage after a selection broke that history. Keeping them is pennies.
Separate the file registration from the sync. When download_asset saves a logo, two things need to happen: the file has to land in the files table (so the agent's ls and cat tools see it) and it has to sync to the preview workload (so the device shows it). The sync is async and can fail without being fatal — the file is correctly stored either way, and the next deploy will re-sync. Coupling the two into a single transaction caused a class of bugs where storage succeeded, sync failed, and the user saw an error for a file that was actually fine.
How this connects to the rest of RapidNative
The image and icon systems don't exist in isolation. They sit alongside the AI chat agent, the real-time preview, the sketch-to-app pipeline, the PRD-to-app flow, and the screenshot-to-code converter. Every one of those entry paths eventually hits the same services described above — because once an app is being generated, it needs the same images and the same icon, regardless of how the generation started.
If you're curious about what else goes on under the hood, we've written up how screenshots become working React Native code and the architecture of our editor and preview pipeline. Both touch on how the image system fits into the broader data flow.
FAQ
How does an AI app builder generate app icons?
Modern AI app builders generate app icons by routing the app description through two stages: first a text model derives three concrete drawable symbols (like "dumbbell" or "stopwatch") from the abstract app concept, then an image model like FAL Sana or Flux Schnell renders each symbol as a flat vector graphic. The icons are generated in parallel, shown to the user as a picker, and the chosen one is written into app.json.
Why not just use DALL·E or Midjourney?
Cost and speed. For an AI mobile app builder generating dozens of images per app, the per-image cost of DALL·E 3 or Midjourney stacks up quickly. FAL Sana at ~$0.001 per megapixel is roughly 50-100x cheaper and returns in under a second, which is the real constraint when images need to appear during live code streaming.
What's the cheapest way to cache AI-generated images?
A two-tier Postgres cache: exact match on a normalized subject key, then a pg_trgm fuzzy match using a GIN index. This works for short subject strings (two to four words) without any external vector service. For longer semantic matching, pgvector with HNSW is the next step up — but for app content images, trigram similarity captures almost everything.
How do you set an Expo app icon from a URL?
Save the image to assets/images/icon.png in the project, update expo.icon in app.json to point at it, and for Android adaptive icons also set expo.android.adaptiveIcon.foregroundImage. The build pipeline reads these at prebuild time and generates the per-density assets automatically.
Try it
If you want to see this system in action — three icons rendered live, images streaming into a running React Native app in seconds — you can start building a mobile app for free on RapidNative. The agent handles everything described in this post out of the box: no API keys to configure, no asset pipelines to wire up, no "upload your logo here" screens unless you want them. Prompt, pick, ship.
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.