platepilot

Android device preview (APK against your dev desktop)

This runbook gets PlatePilot onto a real Android phone while the API, worker, Postgres and LocalStack keep running on the dev desktop. Nothing is exposed to the internet: the phone reaches the desktop through your laptop, over USB (recommended) or your home Wi-Fi.

Why not a public tunnel: the corporate network sinkholes the popular tunnel services (Cloudflare quick tunnels, ngrok, localtunnel, localhost.run, bore, serveo, pinggy all resolve to an internal blackhole). That is a deliberate control, so this runbook does not work around it.

1. Build the APK

On the dev desktop, from the repository root:

scripts/mobile/build-android-apk.sh                       # USB variant, API at http://localhost:3001
scripts/mobile/build-android-apk.sh http://192.168.1.20:3001 wifi   # Wi-Fi variant, laptop LAN address

The build runs inside reactnativecommunity/react-native-android (JDK 17, Android SDK 36, NDK 27, Node 22), so nothing needs installing on the desktop. The first run takes a few minutes; later runs reuse the Gradle cache in .cache/gradle. Output lands in dist/android/platepilot-<variant>-<stamp>.apk with a -latest.apk symlink; both dist/ and .cache/ are gitignored.

What is baked in:

2. Copy the APK to your laptop and install it

# on the laptop
scp <desktop>:~/nutrition-coach/nutrition-coach/dist/android/platepilot-usb-latest.apk .
adb install -r platepilot-usb-latest.apk

adb comes with Android platform-tools (brew install android-platform-tools on macOS). On the phone enable Developer options → USB debugging and accept the laptop’s fingerprint. Without adb you can also copy the file over and open it from the Files app; Android will ask to allow installs from that app.

3. Connect the phone to the desktop

Keep your existing SSH forward from the laptop to the desktop, adding LocalStack so photo uploads work:

ssh -L 3001:localhost:3001 -L 4566:localhost:4566 <desktop>

Then map the phone’s own localhost ports to the laptop:

adb reverse tcp:3001 tcp:3001
adb reverse tcp:4566 tcp:4566

Open PlatePilot. The USB variant already points at http://localhost:3001, and the presigned photo-upload URLs the API hands out point at http://localhost:4566, which the phone now resolves through the same chain. No API configuration change is needed. adb reverse does not survive a reboot or re-plug; run the two commands again if the app reports the server as unreachable.

Option B — Wi-Fi (phone and laptop on the same network)

  1. Find the laptop’s LAN address (ipconfig getifaddr en0 on macOS, hostname -I on Linux), for example 192.168.1.20.
  2. Bind the SSH forwards on all laptop interfaces:

    ssh -L 0.0.0.0:3001:localhost:3001 -L 0.0.0.0:4566:localhost:4566 <desktop>
    

    The laptop firewall must allow inbound 3001 and 4566 from the local network.

  3. Tell the API to write the laptop address into presigned upload URLs, then restart the API container. In the repository .env on the desktop:

    PHOTO_UPLOAD_PUBLIC_ENDPOINT_URL=http://192.168.1.20:4566
    

    The API keeps talking to LocalStack on localhost:4566 itself; only the URL given to the phone changes. The variable is refused in production or without a local AWS_ENDPOINT_URL.

  4. Build and install the Wi-Fi variant: scripts/mobile/build-android-apk.sh http://192.168.1.20:3001 wifi.

Home routers hand out new addresses now and then; if the laptop’s address changes, update .env and rebuild.

4. First run

Troubleshooting

Symptom Cause and fix
App shows “server unreachable” on every screen USB: re-run both adb reverse commands and check the SSH session is alive. Wi-Fi: confirm curl http://<laptop-ip>:3001/health works from the phone’s browser.
Photo upload fails but everything else works Port 4566 is not forwarded (USB: missing adb reverse tcp:4566; Wi-Fi: missing PHOTO_UPLOAD_PUBLIC_ENDPOINT_URL or the API was not restarted after setting it).
INSTALL_FAILED_UPDATE_INCOMPATIBLE A build signed with a different keystore is installed. adb uninstall com.platepilot.mobile and install again.
Build fails with Could not resolve or Connection refused Docker could not reach Maven Central or the npm registry. Retry; the Gradle cache keeps what was already downloaded.
Build fails with an out-of-memory error Raise -Xmx8g in scripts/mobile/build-android-apk.sh, or drop EXTRA_ARCHS.
Android says the app is from an unknown developer Expected: debug keystore. Allow the install for this app only.

Changing the server address without rebuilding

EXPO_PUBLIC_API_BASE_URL is baked into the APK at build time, but a development build can override it on the device: open Me → About and help → Practice switches (local builds) → Server address, enter the full origin (for example http://localhost:3001 behind adb reverse, or a laptop LAN address such as http://192.168.1.20:3001), tap Save, then close and reopen PlatePilot. The value is kept in SecureStore on the device and read once at startup; leave the field empty and save to return to the built-in address. The section shows in development bundles and in APKs built with EXPO_PUBLIC_LOCAL_DEVICE_BUILD=1 (the build script sets it); store builds never show it.