Most AI features in mobile apps die before ship. The ones that make it reduce to four patterns. Here they are with real numbers.
The three constraints that kill 80% of AI features
Before the patterns, the filter. An AI feature ships in a mobile app if and only if it clears these:
- P95 latency under 2s (or streamed with TTFT < 500ms)
- Cost per active user under $0.10/mo (unless paid tier is live day one)
- A degradation story — offline, wrong answer, model down
Every pattern below clears all three. Every pattern that doesn't is why your last AI feature never left staging.
Pattern 1 — Transcribe → Structure
Shape: voice in → structured artifact out (summary, action items, journal entry).
// 1. capture audio (expo-av / expo-audio)
// 2. stream to Whisper / Deepgram / AssemblyAI
// 3. LLM call with a JSON schema
// 4. render + persist
Numbers:
| Stage | P50 | P95 | Cost / 5-min recording |
|---|---|---|---|
| Upload | 400ms | 1.2s | free |
| Whisper transcription | 3s | 7s | $0.03 |
| gpt-4o-mini structuring | 800ms | 2.1s | $0.002 |
The UX trick: stream the transcript in during upload. The user never waits on the wall clock. Perceived latency stays under 1s because the UI is animating continuously.
Reference implementation: the AI Voice Notes template on Applighter ships this end-to-end — 16+ screens, waveform visualizer, pluggable transcription backend.
Pattern 2 — Chat over your data (RAG)
Shape: documents + question → grounded answer.
// ingest (once per doc)
const chunks = chunkPdf(file, { size: 800, overlap: 100 });
const embeddings = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: chunks.map(c => c.text),
});
await supabase.from('doc_chunks').insert(
chunks.map((c, i) => ({ ...c, embedding: embeddings.data[i].embedding }))
);
// query (every message)
const q = await embed(userQuestion);
const { data: matches } = await supabase.rpc('match_chunks', {
query_embedding: q,
match_count: 6,
});
const answer = await streamChat({
messages: [
{ role: 'system', content: `Answer using only:\n${format(matches)}` },
{ role: 'user', content: userQuestion },
],
});
Cost profile — 1,000 users × 20 msg/day:
- Embeddings: ~$4/mo
- LLM (gpt-4o-mini): ~$18/mo
- pgvector on Supabase free tier: $0
- Total: ~$22/mo → $0.022/user
Charge $4.99. Print money.
Supabase's pgvector is the low-ops choice — same Postgres you already use for auth. Pinecone/Weaviate/Chroma only make sense above ~100k docs.
Reference: Supabase vector columns docs.
Pattern 3 — Streaming generation
Shape: prompt → streamed text/JSON.
The transport is the whole game. Same model, same prompt, but token-by-token output shows meaningful content in <300ms. Non-streamed on cellular feels broken; streamed feels instant.
React Native gotcha: default fetch doesn't reliably stream on Android. Use expo/fetch:
import { fetch } from 'expo/fetch';
const res = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ messages }),
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
setStreamedText(prev => prev + parseSSE(decoder.decode(value)));
}
Three things that break in prod:
- Backgrounding. iOS suspends the stream. Buffer server-side, reconnect on resume.
- FlatList reflow. Appending chars re-measures rows every keystroke. Memoize, or use a fixed-height container.
- Interruption. Users tap to type mid-stream. Need cancel-and-restart, not a modal.
Pattern 4 — Multimodal vision extraction
Shape: photo → structured JSON.
const image = await ImagePicker.launchCameraAsync({ base64: true });
const res = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: 'Extract data as JSON matching this schema...' },
{ role: 'user', content: [
{ type: 'text', text: 'Extract line items' },
{ type: 'image_url', image_url: { url: `data:image/jpeg;base64,${image.base64}` } },
]},
],
response_format: { type: 'json_schema', json_schema: RECEIPT_SCHEMA },
});
Gotchas:
- Resize first. iPhone photos are 3–5MB base64. Resize to 1024×1024 max, save 80% of tokens, no accuracy loss.
- The model will guess. Always show the extraction with editable fields before saving.
- P50 is ~3s. Show a skeleton, don't block the camera UI.
Side by side
| Pattern | Model | P50 latency | Cost/event | Ships in |
|---|---|---|---|---|
| Transcribe → structure | Whisper + gpt-4o-mini | 4s | $0.03 | 1 weekend |
| RAG | gpt-4o-mini + pgvector | 1.5s streamed | $0.001/msg | 3–5 days |
| Streaming generation | gpt-4o-mini | 300ms TTFT | $0.0005/reply | 2 days |
| Vision extraction | gpt-4o | 3s | $0.01 | 2–3 days |
The pattern that never ships: agents
Autonomous agents fail silently and expensively. A stuck agent burns $2 of API before anyone notices. A hallucinating agent sends the wrong Slack. On desktop, fine — you're watching a terminal. On mobile — backgrounded, killed, lost network mid-run — it's a support ticket factory.
The four patterns above ship because they're bounded: user sees input, user sees output, loop closes in seconds.
Agents will get there. Not in 2026.
Picking your pattern
- Users speak more than they type → Pattern 1
- Users have their own data → Pattern 2
- Users compose text → Pattern 3
- Users capture from the physical world → Pattern 4
Ship one. Compose later. Never all four at once — that's the roadmap slide of a startup that no longer exists.
Top comments (0)