Recipe — Build a mobile app with Claude Code
For non-developers. This recipe assumes you have no coding experience and are using Claude Code (the terminal AI agent) to drive the work. You don't need to understand any of the code Claude generate…
For non-developers. This recipe assumes you have no coding experience and are using Claude Code (the terminal AI agent) to drive the work. You don't need to understand any of the code Claude generates — but you do need to understand what each step does and what "done" looks like. This page tells you exactly that.
What you'll build: a React Native mobile app for iOS and Android that lets users sign in, connect their wearable, upload labs, and chat with Amy — same agent as the CLI, just a different surface.
Time: ~2 hours, mostly waiting on installs and builds.
Cost: $0 (Expo's free tier covers everything you need to get on your phone).
Before you start
You need three things installed once. If you've done any of them before, skip ahead.
| Tool | What it is | How to install |
|---|---|---|
| Bun | A fast JavaScript runtime (what the Amy CLI runs on) | curl -fsSL https://bun.sh/install | bash |
| Claude Code | The AI agent that will write your code | npm install -g @anthropic-ai/claude-code then claude login |
| Expo Go on your phone | The app that runs your in-progress mobile app, so you can see it without building anything | Download from the App Store / Play Store |
You also need a running Amy backend. If you don't have one yet, follow Deploying to Cloudflare first. You should end that guide with:
- A URL like
https://api.amy.health(orhttps://amy-api.YOUR.workers.dev). - An API key you can use to authenticate as yourself.
If those two things exist, you're ready.
Step 0 — Make a working directory
Open a new terminal and run:
mkdir ~/amy-mobile && cd ~/amy-mobile
claudeYou're now inside Claude Code, in an empty folder. Everything from here on is a prompt you paste into Claude Code's chat. Each one is a self-contained instruction — Claude will ask permission before it does anything destructive.
Step 1 — Scaffold the app
Paste this into Claude Code:
Scaffold a new Expo app in this directory called
amy-mobile, using TypeScript, the Expo Router template (file-based routing, tabs), and Bun as the package manager. After scaffolding, do not start the dev server — just confirm it built and show me the directory tree.
What "done" looks like:
- A new folder named
amy-mobile/exists inside~/amy-mobile/. - It contains an
app/directory with files like_layout.tsxand(tabs)/index.tsx. - Claude reports "scaffold complete" or similar.
If it goes wrong: tell Claude what error you see. Common one: "bun not found" — that means Bun isn't installed (re-do the prerequisites).
Step 2 — Install the Amy SDK
Paste this:
Move into the
amy-mobiledirectory. Install@amy/sdk(the TypeScript SDK for the Amy backend) from npm with bun. Also installreact-native-event-sourcefor SSE streaming andexpo-secure-storefor storing the API key safely on the device. Verify all three were installed by checkingpackage.json.
What "done" looks like:
package.jsonshows@amy/sdk,react-native-event-source, andexpo-secure-storeunder dependencies.
Step 3 — Set up the API client
Paste this: (replace <YOUR_API_URL> with your backend URL from the
prerequisites)
Create a file at
lib/amy.tsthat:
- Reads the API base URL from
EXPO_PUBLIC_AMY_API_URL, defaulting to<YOUR_API_URL>.- Reads the API key from
expo-secure-storeunder the keyamy.api_key.- Exports a function
getAmy()that returns an initializedAmyclient (new Amy({ apiKey, baseUrl })).- Exports a function
signIn(apiKey: string)that stores the key in secure storage.- Exports a function
signOut()that deletes the key.- Also create a
.envfile withEXPO_PUBLIC_AMY_API_URL=<YOUR_API_URL>.
What "done" looks like:
lib/amy.tsexists and exports those four things..envexists in the project root.- Claude added
.envto.gitignore(if not, ask it to).
Step 4 — Build the sign-in screen
Paste this:
Create a sign-in screen at
app/sign-in.tsxthat:
- Has a single text input for "API Key" and a "Sign in" button.
- On submit, calls
signIn(apiKey)fromlib/amy.ts, then navigates to(tabs)/index.- Shows a loading spinner while submitting.
- Shows an error message if the key is invalid (test by calling
amy.me.get()after sign-in; if it throws, show the error).- Use only React Native primitives (View, Text, TextInput, TouchableOpacity, ActivityIndicator) — no UI library.
- Style it simply: white background, centered content, 16px padding.
Also update
app/_layout.tsxso unauthenticated users are redirected to/sign-in(checkexpo-secure-storefor the key on mount).
What "done" looks like:
app/sign-in.tsxexists.app/_layout.tsxredirects when no key is present.
Try it now. Run
bun startinsideamy-mobile/. Scan the QR code with the Expo Go app on your phone. You should see the sign-in screen. Type your API key, tap Sign in, and the app should navigate away (even if the next screen is empty for now).
Step 5 — Build the chat screen (the heart of the app)
Paste this:
Replace
app/(tabs)/index.tsxwith a chat screen that:Layout:
- A scrollable message list filling the screen.
- A text input pinned to the bottom with a "Send" button.
- User messages right-aligned, blue background, white text.
- Amy messages left-aligned, gray background, dark text.
- While Amy is thinking, show the streaming text in real time (italic, slightly dimmer until complete).
Behavior:
- On send: append the user message to local state, clear the input, call
amy.turns.create({ messages })to start a turn, then subscribe toamy.turns.stream(turn.id).- Handle these event types from the stream (see Amy's streaming docs):
turn.started→ add an empty Amy bubble.agent.thoughtwithdelta→ append the delta to the current Amy bubble.turn.completed→ mark the bubble done; show the finalresult.answertext.turn.failed→ show the error message in the bubble in red.- Disable the input while a turn is running.
- Keep all messages in component state for now — no persistence yet.
Use
react-native-event-sourcefor SSE; the SDK exposes the stream URL viaamy.turns.streamUrl(id). The SDK's auth header must be passed manually since EventSource doesn't accept headers natively — use the SDK helperamy.turns.eventSourceInit(id)which returns{ url, headers }and pass them to theEventSourceconstructor.
What "done" looks like:
- You can type a question, tap Send, and watch Amy's answer stream in word by word.
Try it now. Reload Expo Go (shake the phone, tap "Reload"). Ask "what's my average resting heart rate?" — if your backend has data, you should see Amy's answer appear progressively over ~30–90 seconds.
Step 6 — Add the Connect Wearable button
Paste this:
Create a screen at
app/(tabs)/sources.tsxthat:
- Lists currently connected sources (call
amy.sources.list()).- Has a "Connect a wearable" button.
- On tap, calls
amy.sources.terra.connect()to get awidget_url, then opens it inexpo-web-browser'sopenAuthSessionAsync.- After the user completes the OAuth and the browser closes, refreshes the source list.
- Add a tab icon for "Sources" in
app/(tabs)/_layout.tsx.Install
expo-web-browserif it isn't already.
What "done" looks like:
- Tapping "Connect a wearable" opens the Terra widget in a browser.
- After connecting (try with Whoop), the source appears in the list.
Step 7 — Add the Upload Labs button
Paste this:
Create a screen at
app/(tabs)/labs.tsxthat:
- Lists previously uploaded labs (call
amy.labs.list()), showing filename, upload date, and status (processing / parsed / failed).- Has an "Upload labs" button.
- On tap, uses
expo-document-pickerto let the user pick a PDF or image (jpg/png), then callsamy.labs.upload({ file }). The SDK handles the multipart form correctly.- Shows a progress indicator during upload.
- Refreshes the list after upload completes.
- Polls every 5s for status changes on items that are still "processing".
- Add a tab icon for "Labs" in
app/(tabs)/_layout.tsx.Install
expo-document-pickerif it isn't already.
What "done" looks like:
- You can upload a lab PDF, see it appear with "processing", and after ~30s see it flip to "parsed".
Step 8 — Add the Memory screen
Paste this:
Create a screen at
app/(tabs)/memory.tsxthat:
- Lists all facts Amy remembers (call
amy.memory.list()), grouped by category (goal, insight, preference, history).- Each row shows the fact text and a small trash icon to delete (calls
amy.memory.delete(id)).- Pull-to-refresh.
- Has a "+" button in the header that opens a modal to add a new fact (
amy.memory.create({ text, category })).- Add a tab icon for "Memory" in
app/(tabs)/_layout.tsx.
What "done" looks like:
- Memory screen shows your saved facts from the backend.
Step 9 — Polish and test
Paste this:
Do a polish pass:
- Add an Amy logo placeholder in the header (just text "Amy" for now).
- Ensure all screens handle the loading state (spinner) and the error state (a small red banner with the message).
- Test the full flow on your phone end-to-end: sign in → connect wearable → upload a lab → ask a question that uses both data sources → see the answer stream.
- Report any bugs you hit.
What "done" looks like:
- The four-tab app (Chat, Sources, Labs, Memory) works end-to-end on your phone via Expo Go.
Step 10 — Ship it (optional)
You now have a working app running on your phone via Expo Go. To put it in someone else's hands without making them install Expo Go, you have two options:
| Option | What it gives you | Cost | Effort |
|---|---|---|---|
| EAS Update | Push updates to anyone who has your app installed | Free for small teams | 5 min |
| EAS Build | Generate .ipa (iOS TestFlight) and .apk (Android sideload) files | Free tier; paid past 30 builds/mo | 30 min |
Either is a one-shot Claude Code prompt. When you're ready, ask:
Set up EAS Build for this project so I can distribute the iOS and Android builds to a few testers. Walk me through every CLI prompt.
Common problems and fixes
"Network request failed" on every API call
Your phone (running Expo Go) can't reach localhost. You need a
publicly reachable backend URL. Either:
- Use your deployed Cloudflare backend (recommended) — update
EXPO_PUBLIC_AMY_API_URLin.env. - Tunnel localhost via
bunx cloudflared tunnel --url http://localhost:8787if you're running the backend locally.
Stream stops after ~30 seconds
Some networks idle out SSE connections. The SDK auto-reconnects with
Last-Event-Id, but if you see this, mention it to Claude — they can
add explicit reconnect logging to confirm it's recovering.
"Invalid API key" on sign-in
The API key you're using is wrong, expired, or for a different backend.
Get a fresh one with amy whoami --print-key from the CLI.
What you've built
┌────────────────────────────────────┐
│ Your phone (iOS or Android) │
│ Expo Go app running amy-mobile │
└──────────────────┬─────────────────┘
│ HTTPS
▼
┌────────────────────────────────────┐
│ api.amy.health (Cloudflare) │
│ Same backend the CLI uses. │
└────────────────────────────────────┘The mobile app is a thin client over the Amy backend. The agent doesn't run on the phone — it runs on Cloudflare. The phone just shows the results.
This is the whole point of the design: swap the surface, keep the brain. Tomorrow you can build a web app or a Slack bot the same way.
Where to next
- Learn how the agent works under the hood: Internals: Agent orchestration.
- Add a new wearable that Terra doesn't support yet: Add a new adapter.
- Make the chat feel as alive as the CLI: Stream events.
Recipe — Upload a lab report
Goal: let a user upload a PDF (or phone photo) of their bloodwork, wait for it to be OCR'd and parsed into structured biomarkers, and surface "out of range" markers in your UI.
Build a web app
A working web client for Amy. Next.js or Vite, the same API as the CLI, streaming SSE wired in so the user watches Amy think in real time.