Skip to content

Local Development

How to run the native iOS and Android apps locally. Use this when you need fast iteration — code change to running app in seconds rather than minutes.

Two local-dev paths exist:

  1. Expo Go — covered in Frontend → Setup. Quickest start; works for everything that doesn't need native modules beyond Expo's standard set.
  2. expo run:android / expo run:ios — native development build on simulator/emulator/device. Required when you've added a native module that Expo Go doesn't include (Google Sign-In, native maps with Google Maps SDK, etc.). The binary is built once and the JS bundle hot-reloads against it; rebuild only on a native change.

This page covers path 2 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:

  1. Checks for a connected Android device or running emulator. If none is found, prints an error.
  2. Compiles the native Android code (Gradle build, takes a few minutes the first time, faster on subsequent runs).
  3. Installs the APK on the device.
  4. 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

  1. GCP Console → APIs & Services → Credentials (in project development-485000).
  2. Open the Maps SDK for Android API key.
  3. Application restrictionsAndroid apps → add an entry:
    • Package name: com.tomoda.app
    • SHA-1 fingerprint: paste the value above.
  4. 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 → SettingsPlatforms → 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
- Left sidebar → click the blue Tomoda project icon - Middle pane → Tomoda target → Signing & Capabilities tab - Team dropdown → pick your paid team (or Add an Account… to sign in with the Apple ID associated with your Developer Program membership) - Leave Automatically manage signing checked

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 AccessloginCertificates → 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.js plugins 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:

  1. open frontend/ios/Tomoda.xcworkspace
  2. Top toolbar → destination dropdown (between the scheme name and the play button) → pick a Simulator (e.g. iPhone 17 Pro)
  3. 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:

  1. Runs npx expo prebuild --clean --platform ios so the ios/ project tree reflects current app.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 regenerates ios/ from scratch.
  2. Opens ios/Tomoda.xcworkspace in Xcode.
  3. Starts Metro with --dev-client in 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).

Reusing the dev client across sessions

The binary that task dev:android / task dev:ios installs is a dev client: once it's on your device/emulator you don't need to rebuild it for JS changes. Run task dev:frontend and connect — your JS bundle hot-reloads against the installed dev client's native runtime. Rebuild the binary (expo run:*) only when you change native code or add a native module.

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 frontend first.
  • Network error on login — emulator can't reach localhost. Use 10.0.2.2 for 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 -version shows 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.