Skip to content

Mobile apps (iOS / Android)

The phone apps are the same Tauri shell as the desktop app (apps/shell). The web build runs in the system webview, keeps its whole workspace locally (IndexedDB + Loro), and syncs through any of the usual channels: a server, a LAN peer, or a file pack. Nothing in the study features is desktop-specific.

iOS

Prerequisites: Xcode with iOS simulators, and a Rust toolchain with the iOS targets. Two ways to get the toolchain:

  • Nix: nix develop gives it to you pinned (the flake includes the iOS targets). Nothing else to install.
  • Or rustup: rustup target add aarch64-apple-ios aarch64-apple-ios-sim.

Either way Xcode itself is not in Nix (Apple does not allow it), so the iOS SDK, the simulator and signing always come from your Xcode install.

The Xcode project lives in apps/shell/src-tauri/gen/apple and is committed, so there is nothing to generate. Avoid re-running pnpm tauri ios init: it overwrites the project, including the Info.plist local-network keys that LAN sync needs. Only run it to regenerate after changing plugins, and re-add those keys afterwards.

Add an Apple account once

iOS builds must be signed, even for the simulator in a release build and always for a device. Do this one-time setup or the build fails with "requires a development team" / "No Accounts":

  1. Open Xcode > Settings > Accounts and add an Apple ID (a free one works for local development). Signing for the simulator and your own devices works with the personal team that gives you.
  2. Find the team id: Xcode > Settings > Accounts (the team's id), or security find-identity -v -p codesigning (the code in parentheses after "Apple Development: Your Name").
  3. Put it in a gitignored .env.local at the repo root, which the Nix shell loads automatically:
echo 'APPLE_DEVELOPMENT_TEAM=XXXXXXXXXX' > .env.local

Outside the Nix shell, export APPLE_DEVELOPMENT_TEAM=XXXXXXXXXX before building. Tauri reads it and sets the Xcode development team.

To sign against the Synapse team (for a build distributed as Synapse rather than under your own name), ask to be added to it. For now that team is @dhovart's personal one, so this is a manual invite rather than something you can self-serve.

Build and run

Run this from a plain terminal, not nix develop:

cd apps/shell
pnpm ios:sim

It creates/boots a simulator matching the Xcode SDK and runs the app on it. Simulators need no signing, so it skips the device setup below. iOS uses the system Xcode toolchain, so it must run outside nix develop (only Android builds inside the Nix shell).

Distribution goes through the normal Xcode archive flow (TestFlight / App Store). A device or App Store build must be signed by the Synapse team, so ask to be added to it (see above); CI signs headlessly from that team's App Store Connect API key (APPLE_API_ISSUER, APPLE_API_KEY, APPLE_API_KEY_PATH).

Custom Info.plist keys

The LAN-sync keys (NSLocalNetworkUsageDescription, NSBonjourServices) live in apps/shell/src-tauri/Info.ios.plist, pointed at from tauri.conf.json (bundle > iOS > infoPlist). Tauri merges it at build time. Don't put them in the generated gen/apple/synapse_iOS/Info.plist; tauri ios init overwrites it.

Android

The whole Android toolchain is pinned in the flake, so nix develop is the only prerequisite: it provides the Rust cross-targets, a JDK, and the Android SDK/NDK/build-tools (via androidenv), and exports JAVA_HOME, ANDROID_HOME and NDK_HOME. No Android Studio install is required, and no ANDROID_HOME to set by hand. (The SDK is unfree; the flake accepts its licence for this dev shell only.)

The Android project in apps/shell/src-tauri/gen/android is committed, so there is nothing to generate. Run the Tauri commands inside nix develop:

# run on an emulator or connected device
nix develop --impure -c pnpm --filter @synapse/shell tauri android dev

# build a signed universal APK
nix develop --impure -c pnpm --filter @synapse/shell tauri android build --apk

tauri android dev needs a running emulator (or a connected device); start one first (emulator -avd <name>), and make sure its disk has room (a failed install with "not enough space" is a full emulator, not a build error).

Signing

tauri android build signs the release APK from gen/android/app/keystore.properties (gitignored). Point it at a keystore:

storeFile=/absolute/path/to/keystore.jks
storePassword=
keyAlias=
keyPassword=

For local installs the Android debug keystore works (~/.android/debug.keystore, all three secrets android / alias androiddebugkey). Without this file the release build is left unsigned, and Android refuses to install an unsigned APK. The build tool version is pinned for every module (app and the Tauri plugin subprojects) in gen/android/build.gradle.kts so the Android Gradle plugin never tries to download into the read-only Nix store.

The signed APK lands at gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk; install it with adb install -r <that path> or by sideloading the file.

What differs from desktop on a phone

  • The UI switches to its narrow layouts (the viewer replaces the side panels with a bottom toolbar); the e2e suite covers this with a phone-viewport pass (apps/web/e2e/specs/mobile.spec.ts).
  • LAN sync: the Rust side (mDNS + WebSocket listener) compiles for mobile as part of the same crate. iOS prompts for the local-network permission (NSLocalNetworkUsageDescription and NSBonjourServices are set in the committed Info.plist) and Android needs the app in the foreground while syncing.
  • File packs: exporting opens the system save picker. On Android that returns a Storage Access Framework location (no filesystem path), so the app confirms the saved file by name rather than a path, and the desktop "show in folder" action is hidden there.