Skip to content

Mobile builds

The Android and iOS apps are the web console wrapped in Capacitor 7, plus a native call plugin that gives incoming calls a real OS call experience.

App identity

From frontend/capacitor.config.ts:

  • appId: com.towit.links
  • appName: TowitLinks
  • webDir: build — the static output of pnpm build:app
  • A SplashScreen configuration, and a BackgroundRunner job (runners/calls.service.js) that repeats every 15 seconds with autoStart (:12-19)

The version lives in frontend/android/version.properties, not in package.json.

The native call plugin

CallPlugin (frontend/src/lib/plugins/call_plugin.ts:33) is the bridge. Its Android implementation is Kotlin under frontend/android/app/src/main/java/com/towit/links/plugins/:

File Role
CallPlugin.kt The Capacitor plugin surface
services/CallService.kt Foreground service keeping the call alive
call/CallListener.kt Twilio call events
CallBroadcastReceiver.kt System intents — answer, reject, hang up from the notification
sound/SoundPoolManager.kt Ringtone and in-call tones

The iOS counterpart is under frontend/ios/App/App/plugin. Call hold is implemented here and only here; the web client returns false from _toggleCallOnHold().

Local device builds

cd frontend
pnpm build:android     # vite build && cap sync android && cap open android

For hot reload on a device, uncomment the server.url block in capacitor.config.ts:21-28: use http://10.0.2.2:5001 for the Android emulator, or your workstation's LAN IP for a physical device or iOS. frontend/android.md has the longer walkthrough, including the network_security_config.xml cleartext exception and npx capacitor-assets generate for icons and splash screens.

Gradle shortcuts exist as npm scripts: assemble:debug, assemble:release and bundle:release.

Release builds

Releases are produced on a dedicated build host (154.53.40.104, repository at ~/build_apk/VoIP), triggered manually through .github/workflows/android.yml, which SSHes in and runs rebuild_android.sh.

What the script does:

  1. git pull, then read frontend/android/version.properties
  2. Build the image voip_apk_builder from frontend/Dockerfile.build_apk
  3. Run it with the repository bind-mounted at /app. Inside the container (Dockerfile.build_apk:49-66): npm install --legacy-peer-deps, npm run build:app, npx cap sync android, then gradlew assembleDebug, assembleRelease, bundleRelease and incrementSubversion
  4. Collect the APK, AAB, mapping file and debug symbols into android_outputs/output_<name>_<code>/, zipping the stripped symbols
  5. Commit and push the bumped version.properties (rebuild_android.sh:67-69)
  6. Prune Docker

Step 5 means the build host writes to the repository. A release therefore produces a commit you did not make locally; pull before starting new work after a build.

Artifacts are served from the staging host at staging.towitlinks.com/android/.

Signing

The release keystore is towitlink.jks. It is no longer in the repository — it was removed because a signing key in version control is a signing key anyone with repository access can use. Keep a copy outside the repository and place it on the build host manually.

The keystore password is still documented in plain text

frontend/android.md:22 contains the keystore password in the example command. An Android signing key cannot be rotated for an app already published without Play App Signing — the key is the app's identity. Treat both the keystore and that password as compromised and decide, with the team, what the remediation is. See Known issues.