Local Development¶
How to run the native iOS and Android apps locally without going through EAS. Use this when you need fast iteration — code change to running app in seconds rather than minutes, with no EAS build queue.
Three local-dev paths exist:
- Expo Go — covered in Frontend → Setup. Quickest start; works for everything that doesn't need native modules beyond Expo's standard set.
expo run:android/expo run:ios— native development build on simulator/emulator. Required when you've added a native module that Expo Go doesn't include (Google Sign-In, native maps with Google Maps SDK, etc.).- EAS dev client — a custom dev binary installed once, then JS bundle hot-reloads against it. Best for ongoing native-module work without rebuilding the binary each time.
This page covers paths 2 and 3 on macOS, since Tomoda's primary native dev surface is Android (more common configuration friction). iOS local dev is shorter — mostly Xcode + a single command — and gets a brief section at the end.
Android — toolchain¶
You need three things working in concert: Java, the Android SDK, and an emulator (or a tethered physical device).
| Tool | Required version | How to install |
|---|---|---|
| JDK | OpenJDK 17 | brew install openjdk@17 |
| Android Studio | Latest stable | Download |
| Android SDK Platform-Tools | Latest | Android Studio → SDK Manager → SDK Tools tab |
| Android Emulator + a system image | Latest | Android Studio → Virtual Device Manager |
| Node | 18+ | brew install node |
Wire JDK 17 into the system¶
brew install puts the binaries in place but doesn't make them the system default. Symlink it:
sudo ln -sfn /usr/local/opt/openjdk@17/libexec/openjdk.jdk \
/Library/Java/JavaVirtualMachines/openjdk-17.jdk
Verify:
java -version
# openjdk version "17.0.x"
Set environment variables¶
Add to ~/.zshrc (or ~/.bashrc):
# Java
export JAVA_HOME="/usr/local/opt/openjdk@17"
# Android SDK — default Mac install location
export ANDROID_HOME="$HOME/Library/Android/sdk"
# CLI tools on PATH
export PATH="$ANDROID_HOME/emulator:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$PATH"
Reload your shell: source ~/.zshrc.
M2 Macs running Rosetta Homebrew
If brew --prefix openjdk@17 returns a /usr/local/... path rather than /opt/homebrew/..., your Homebrew is installed under Rosetta (Intel-compatible mode). This is fine — the paths above already match. If you migrated to a native Apple Silicon Homebrew, swap /usr/local for /opt/homebrew everywhere.
Create an emulator¶
Android Studio → Tools → Device Manager → Create Virtual Device.
| Setting | Pick |
|---|---|
| Device | Pixel 9 (or any reasonably recent phone profile) |
| System image | API 35 (Android 15) |
| Graphics | Hardware - GLES 2.0 |
Boot it once before running anything else — first boot can take 60+ seconds.
Android — running the app¶
From the repo root:
task dev:android # or: cd frontend && npx expo run:android
What this does:
- Checks for a connected Android device or running emulator. If none is found, prints an error.
- Compiles the native Android code (Gradle build, takes a few minutes the first time, faster on subsequent runs).
- Installs the APK on the device.
- Starts Metro and connects.
After the first install, daily work is task dev:frontend and press a in Metro to attach to the existing APK on the running emulator. Fast Refresh applies JS edits without rebuilding. Re-run task dev:android only after a native dep change or an app.json plugin update.
Pointing the app at your local backend¶
The Android emulator runs in its own VM — localhost from inside the emulator is the emulator itself, not your Mac. Use the loopback address 10.0.2.2 to reach services running on the host:
EXPO_PUBLIC_API_URL=http://10.0.2.2:8080/api/v1 npx expo run:android
A physical tethered device sees your Mac directly via its LAN IP (192.168.1.x etc.).
Choose a specific target¶
# List devices visible to ADB
adb devices
# Build for a specific device id
npx expo run:android --device <id>
# Build a release variant locally (signed with the debug keystore)
npx expo run:android --variant release
Reset Metro cache when things get weird¶
rm -rf $TMPDIR/metro-* $TMPDIR/haste-map-*
task dev:frontend:clear
Android — Google Maps integration¶
The map screens use the Google Maps SDK for Android, which is gated on a SHA-1 fingerprint registered in the Google Cloud Console. Without this, map tiles render as a gray grid.
Get the debug keystore SHA-1¶
keytool -list -v -keystore ~/.android/debug.keystore \
-alias androiddebugkey \
-storepass android \
-keypass android \
2>/dev/null \
| grep 'SHA1:'
You'll get something like:
SHA1: 5E:8F:16:06:2E:A3:CD:2C:4A:0D:54:78:76:BA:A6:F3:8C:AB:F6:25
Register it in Google Cloud Console¶
- GCP Console → APIs & Services → Credentials (in project
development-485000). - Open the Maps SDK for Android API key.
- Application restrictions → Android apps → add an entry:
- Package name:
com.tomoda.app - SHA-1 fingerprint: paste the value above.
- Package name:
- Save. Map tiles should render on next app reload (allow a minute for propagation).
Each developer registers their own debug SHA-1. The production SHA-1 (used for Play-signed builds) is registered separately by whoever runs the release pipeline — see Play Store.
iOS — toolchain¶
| Tool | Required | How |
|---|---|---|
| Xcode | Latest stable | App Store |
| Xcode command-line tools | Bundled | xcode-select --install once after Xcode |
| CocoaPods | Latest | sudo gem install cocoapods |
| iOS Simulator runtime | Bundled with Xcode | Open Xcode once → Settings → Platforms → install a recent iOS version |
| Apple Developer Program | Recommended (paid) | https://developer.apple.com/programs/ |
xcode-select -p must print /Applications/Xcode.app/Contents/Developer. If it prints the bare CLI tools path, fix with:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept
iOS — one-time per-developer setup¶
The frontend/ios/ folder is gitignored; each developer regenerates it locally via Expo prebuild. Per-developer state (signing team, certificates, provisioning profiles) lives in the local ios/Tomoda.xcodeproj and never touches the repo. Shared fixes (Podfile patches, plugin config) live in frontend/plugins/ and frontend/app.config.js and apply automatically on every prebuild.
1. Pick a signing team in Xcode¶
Required even for Simulator-only builds (Xcode 16+ refuses to build without a team).
open frontend/ios/Tomoda.xcworkspace
The Team selection is stored in the local (gitignored) project.pbxproj. Nothing to commit.
2. Install an Apple Development certificate¶
Xcode → Settings (⌘,) → Accounts → click your Apple ID → Manage Certificates… → click + in the bottom-left → Apple Development.
The cert + private key land in the login keychain. Verify with:
security find-identity -v -p codesigning
# expect: "Apple Development: Your Name (TEAMID)"
If you see 0 valid identities found, the private key didn't save. Open Keychain Access → login → Certificates → delete any key-less Apple Development entries, then redo the Manage Certificates → + step and click Always Allow on the keychain prompt.
iOS — running the app¶
Pick one based on where you want the binary:
| Target | Task | What it does |
|---|---|---|
| Simulator (default) | task dev:ios |
Boots an iPhone Simulator if none is up, runs expo run:ios for a clean build + install + Metro attach |
| Connected iPhone | task dev:ios:device |
Runs expo prebuild, opens the Xcode workspace, starts Metro with --dev-client. You press ⌘R in Xcode to build & install on the phone |
The Simulator wrapper verifies the toolchain (Xcode path, xcrun simctl, CocoaPods) before handing off. First build takes ~3–5 minutes either way; the dev binary is installed and Metro attaches.
After the first install on the Simulator, daily work is:
task dev:frontend # starts Metro
# press i → opens the already-installed dev binary
On a connected iPhone, the Xcode-based flow is the same as Simulator: task dev:frontend after the first task dev:ios:device and the app reconnects to Metro on its own (assuming the phone and Mac share a Wi-Fi network).
Fast Refresh applies JS edits in <1 second. Re-run task dev:ios (or task dev:ios:device) only when you:
- Add a native dependency (
npm install some-native-pkg) - Change
app.json/app.config.jsplugins or any native config - Bump Expo SDK or React Native
Fallback: build via Xcode directly¶
If task dev:ios fails at the xcodebuild step on a new iOS Simulator runtime (Expo CLI sometimes misclassifies brand-new runtimes as physical devices and demands provisioning), bypass it:
open frontend/ios/Tomoda.xcworkspace- Top toolbar → destination dropdown (between the scheme name and the play button) → pick a Simulator (e.g. iPhone 17 Pro)
- Press ⌘R
Xcode builds and installs the dev binary on the Simulator. From then on, task dev:frontend + i reattaches without rebuilding.
Choose a tethered iPhone instead¶
Two paths. Task path (recommended — handles prebuild + Metro + Xcode in one go):
task dev:ios:device
The task:
- Runs
npx expo prebuild --clean --platform iosso theios/project tree reflects currentapp.config.js+ plugins (new native deps land here). The clean regen is required because the Share Extension target (injected by@bacons/apple-targets) crashes on an in-place update, so the task always regeneratesios/from scratch. - Opens
ios/Tomoda.xcworkspacein Xcode. - Starts Metro with
--dev-clientin the same terminal.
In Xcode, pick your iPhone from the destination dropdown and press ⌘R to build & install. When the binary launches on the phone it connects to the Metro bundler that's already running in your terminal. JS-only changes after that just need a save; re-run task dev:ios:device only when a new native module gets added.
Direct expo path (skips Xcode UI, fine if your signing setup is already happy):
cd frontend && npx expo run:ios --device
Picks a connected device interactively.
Either path requires: the device registered to your team (Xcode → Window → Devices and Simulators → your iPhone → Use for Development), Developer Mode on the iPhone (Settings → Privacy & Security → Developer Mode), and the iPhone trusting this Mac ("Trust This Computer" tapped after the first plug-in).
Local backend from the Simulator¶
The iOS Simulator shares the host network, so http://127.0.0.1:8080 Just Works (no 10.0.2.2-style indirection like Android).
EAS dev client (recommended for ongoing native work)¶
If you're going to be developing against native modules over multiple sessions, build an EAS dev client once and use it instead of repeating expo run:android / expo run:ios:
eas build --profile development --platform android # or ios
Install the resulting binary onto your device/emulator. Then run task dev:frontend and connect — your JS bundle hot-reloads against the dev client's native runtime without rebuilding the binary.
See Native Testing for using dev clients in QA workflows.
Common pitfalls¶
iOS¶
| Symptom | Cause | Fix |
|---|---|---|
xcrun simctl help exited with non-zero code: 72 |
xcode-select points at bare CLI tools, not Xcode |
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer |
No code signing certificates are available to use |
No Apple Development cert in the login keychain |
Xcode → Settings → Accounts → Manage Certificates → + → Apple Development |
No profiles for 'com.tomoda.app' were found while building for a Simulator |
Expo CLI misclassified a new iOS Simulator runtime as a physical device | Use the Xcode fallback above: open Tomoda.xcworkspace, pick a Simulator destination, ⌘R |
simctl openurl ... exited with non-zero code: 60 |
Dev binary not installed; exp:// URL has no handler |
Run task dev:ios once to install the binary, then use task dev:frontend + i |
'react/utils/Telemetry.h' file not found |
RN / Pods version drift after an accidental npm i ...@latest rewrite of package.json |
git checkout frontend/package.json frontend/package-lock.json, then rm -rf frontend/{node_modules,ios} && npm install and retry task dev:ios |
Swift pod (AppCheckCore) refuses to integrate as static lib |
GoogleUtilities / RecaptchaInterop need modular headers |
Handled automatically by frontend/plugins/with-modular-headers.js on every prebuild; if you deleted the plugin, re-add it |
| Cert exists but Xcode still demands signing | Team on the target doesn't match the cert's team | Signing & Capabilities → set Team to the same (TEAMID) shown in security find-identity -v -p codesigning |
Android¶
- "No connected devices" — open Android Studio → Device Manager → Play your emulator manually before running
task dev:android. - "Task 'installDebug' not found" — you're not in the
frontend/folder.cd frontendfirst. - Network error on login — emulator can't reach
localhost. Use10.0.2.2for the API URL. - Map tiles render as gray — debug keystore SHA-1 not registered in Cloud Console. See the Google Maps section above.
- JDK mismatch on Gradle build — confirm
java -versionshows 17, not 11 or 21.
What this page does NOT cover¶
- The release pipeline — see Native Release for the EAS Build/Submit flow.
- Store portal management — see Play Store for Google Play Console, App Store for App Store Connect.
- Testing strategy — see Native Testing for emulator/device QA flows and OAuth caveats.