Ops Mobile Apps - lukislp/mowdeck GitHub Wiki
The MowDeck apps (apps/mobile, ADR 0036) are Capacitor shells around the web app. They do not contain a copy of it: on first start they ask for your MowDeck server's address and from then on load the web app from that server. A server update reaches the apps at once, and login works exactly like in the browser.
| iPhone | Android | |
|---|---|---|
| Install | built and installed from your Mac (paid Apple developer account, no TestFlight, no App Store) | APK from the GitHub release |
| Notifications, app open | yes (local notifications from the inbox, icon badge) | yes (local notifications from the inbox, icon badge where the launcher supports it) |
| Notifications, app in background or closed | Apple Push (APNs) straight from your server, needs your APNs key on the server | ntfy app (no Firebase) |
| Deep links | mowdeck://mowers/<id> |
mowdeck://mowers/<id> |
-
HTTPS with a certificate the phone trusts. Login uses a
Securecookie, so plain HTTP only gets you to the login page. A public certificate (for example Tailscale HTTPS or Let's Encrypt) works everywhere. With a private CA, install and trust its certificate on the phone: iOS Settings → General → VPN & Device Management (install the profile), then Settings → General → About → Certificate Trust Settings (switch it on); Android Settings → Security → Encryption & credentials → Install a certificate → CA certificate (the MowDeck app trusts user CAs). - Reachable from the phone: same Wi-Fi, or a VPN (Tailscale, WireGuard) when away.
- For iPhone background notifications: the APNs key, see Apple Push.
Prerequisites on the Mac: Xcode (from the App Store, opened once, sudo xcode-select -s /Applications/Xcode.app), Node.js 22 with corepack enable (pnpm), git and a clone of this repository. Everything below also works over SSH from the PC.
-
Apple ID of the paid team in Xcode: Xcode → Settings → Accounts → "+" → Apple ID. Note the Team ID (Xcode → Settings → Accounts → your team, or developer.apple.com → Membership details).
-
Settings file on the Mac (not in the repository):
cat > ~/.mowdeck-ios.env <<'EOF' MOWDECK_APPLE_TEAM_ID=ABCDE12345 EOF
Optional:
MOWDECK_DEVICE_ID=<udid>when more than one iPhone is paired,MOWDECK_VERSION=1.2.3to override the version (default: the newestv*tag). -
Keychain password for SSH sessions (stays on the Mac):
printf '%s' 'MAC-LOGIN-PASSWORD' > ~/.mowdeck-sign-pass && chmod 600 ~/.mowdeck-sign-pass. SSH sessions get a locked keychain; the scripts unlock it with this file. -
iPhone: connect it by USB once, unlock it and tap "Trust this computer". Switch on Developer Mode: Settings → Privacy & Security → Developer Mode → on, restart, confirm. For later installs over Wi-Fi, keep the iPhone on the same network as the Mac (Xcode → Window → Devices and Simulators → "Connect via network" if it does not show up).
-
Provision (creates the Apple Development certificate, registers the iPhone, creates the development profile for
app.mowdeck.mobilewith Push Notifications and Time Sensitive Notifications):bash apps/mobile/scripts/provision.sh
It ends with the profile's expiry date and
APNs: development. If it saysMISSING, the team is not a paid account or the capability could not be added; openapps/mobile/ios/App/App.xcodeprojin Xcode once, select the team under Signing & Capabilities and run the script again.The bundle id
app.mowdeck.mobilemust be free in Apple's developer portal. If another team already uses it, changeappIdinapps/mobile/capacitor.config.ts,PRODUCT_BUNDLE_IDENTIFIERin the Xcode project,BUNDLE_IDinapps/mobile/scripts/common.shandMOWDECK_APNS_BUNDLE_IDon the server to one of your own.
bash apps/mobile/scripts/build-ios.sh # cap sync ios + xcodebuild archive (signed)
bash apps/mobile/scripts/sign-and-install.sh # signature check + devicectl install (USB or Wi-Fi)sign-and-install.sh prints the APNs environment of the build and the profile's expiry. Data and login are kept on every update. Because the web app comes from the server, you only need a new build for native changes (a new MowDeck release with changes in apps/mobile).
- If iOS says "Untrusted Developer": Settings → General → VPN & Device Management → your developer certificate → Trust. (With a paid team and a development profile this usually is not needed; it is needed once per team on a new phone.)
- The app asks for the server address, for example
mowdeck.home.lanorhttps://mowdeck.example.org. It checks that a MowDeck server answers there (/manifest.webmanifest); "Use anyway" skips the check. - Log in as in the browser. The app asks once for permission to send notifications; allow it.
- To switch servers later: Settings → Notifications → "This app" → "Change server". If the server is unreachable at start, the app shows "Server not reachable" with "Try again" and "Choose another server".
Paid profiles and development certificates are valid for a year. There is no 7-day renewal (that only applies to free Personal Team signing, which cannot use push), so no launchd agent is needed. Before the date sign-and-install.sh prints: run provision.sh, then build-ios.sh and sign-and-install.sh again (or MOWDECK_RESIGN=1 bash apps/mobile/scripts/sign-and-install.sh to re-sign the existing build with the new profile).
The Notifications service sends straight to Apple with your APNs auth key (token-based, no Firebase, no third party):
- developer.apple.com → Certificates, Identifiers & Profiles → Keys → "+" → name it, tick Apple Push Notifications service (APNs), choose "Sandbox & Production", Continue, Register, Download
AuthKey_<KEYID>.p8(only once possible). Note the Key ID. - Give it to the server:
-
Compose: copy the file to
infra/compose/secrets/apns/AuthKey_<KEYID>.p8and set ininfra/compose/.env:APNS_KEY_FILE=/run/secrets/apns/AuthKey_<KEYID>.p8 APNS_KEY_ID=<KEYID> APNS_TEAM_ID=<TEAMID> APNS_BUNDLE_ID=app.mowdeck.mobile APNS_ENVIRONMENT=development
then
docker compose up -d notifications. -
Helm: create a Secret with the key (
kubectl -n mowdeck create secret generic mowdeck-apns --from-file=AuthKey.p8=AuthKey_<KEYID>.p8) and setservices.notifications.apns.enabled=true,existingSecret=mowdeck-apns,keyId,teamId(andbundleId,environmentif they differ).
-
-
developmentis right for apps installed from the Mac: a build with a development profile registers with the APNs sandbox.productionis only for TestFlight or App Store builds. - Check: the notifications log says
notification channels ... apns; Settings → Notifications in the app shows "Apple Push: on"; "Send test notification" arrives with the app closed.
Device tokens are stored per user (POST /api/v1/notifications/me/push/apns) and removed on logout and when Apple reports them as unregistered. The channel "iPhone app" can be switched off in the notification preferences like Web Push or ntfy; quiet hours apply, theft alarms break through them and through Focus (time-sensitive).
- Download
mowdeck-<version>.apkfrom the GitHub release (Assets) on the phone. - Open it; Android asks to allow installs from this source (browser or file manager) once. Install.
- Updates: install the newer APK over the old one; data and login stay. Every release APK is signed with the same release key, so updates work; a debug-signed APK you built yourself cannot update a release one (uninstall first).
First start, server address and "Change server" work as on the iPhone. On Android 13 and later the app asks for permission to show notifications after login.
Android has no system push without Google's Firebase Cloud Messaging, and MowDeck does not use it. Instead:
- While the app is open (and for a while in the background, until Android suspends it) it shows every new notification from your inbox itself.
-
In the background, ntfy: install the ntfy app (Google Play, F-Droid, see ntfy.sh/docs/subscribe/phone), set up a topic in MowDeck under Settings → Notifications → ntfy, then tap "Subscribe in the ntfy app" in the "This app" panel (opens
ntfy://<server>/<topic>) or subscribe to the same server and topic in ntfy by hand. Once an ntfy target exists, the MowDeck app stops showing its own notifications so nothing arrives twice.
Prerequisites: Node.js 22 with pnpm, JDK 21, the Android SDK (ANDROID_HOME; Android Studio installs it).
bash apps/mobile/scripts/build-android.sh # -> apps/mobile/build/mowdeck-<version>.apkWithout a keystore the APK is debug-signed. Release signing:
-
Create the keystore once and keep it safe (losing it means users must uninstall to update):
keytool -genkeypair -v -keystore mowdeck-release.jks -alias mowdeck -keyalg RSA -keysize 4096 -validity 10000
-
Locally:
MOWDECK_ANDROID_KEYSTORE=mowdeck-release.jks MOWDECK_ANDROID_KEYSTORE_PASSWORD=... MOWDECK_ANDROID_KEY_ALIAS=mowdeck bash apps/mobile/scripts/build-android.sh. -
For the release workflow, add repository secrets:
ANDROID_KEYSTORE_BASE64(base64 -w0 mowdeck-release.jks),ANDROID_KEYSTORE_PASSWORD,ANDROID_KEY_ALIAS, optionallyANDROID_KEY_PASSWORD(defaults to the keystore password). The jobandroidofrelease.ymlbuilds the APK on ubuntu for everyv*tag and the release attaches it; without the secret the job only warns and the release has no APK. Pull requests never build the native apps (minute budget); the CI jobwebruns the mobile package's lint,tsccheck and tests.
The artwork is the web app's icon. To change it, edit apps/mobile/scripts/make-resources.py, then:
uv run python apps/mobile/scripts/make-resources.py # sources in apps/mobile/resources/
pnpm --filter mobile assets # every iOS and Android size (@capacitor/assets)On Windows @capacitor/assets can fail with EBUSY on mipmap-anydpi-v26/ic_launcher_round.xml; delete the two files in that folder and run it again.
| Symptom | Cause and fix |
|---|---|
| "No MowDeck server answers at this address" | Wrong address, phone not in the network/VPN, or a certificate the phone does not trust (see What the server needs). |
| Login page shows, login fails | The server is on plain HTTP; the session cookie needs HTTPS. |
| "Apple Push: failed" | No network to Apple, or the build has no aps-environment (re-run provision.sh, check its APNs: line). |
| Apple Push on, but nothing arrives in the background | Server without APNs key (the panel says so), APNS_ENVIRONMENT=production for a Mac-installed build (must be development), or a different bundle id on server and app. The notifications log names Apple's reason (BadDeviceToken, TopicDisallowed, ...). |
errSecInternalComponent over SSH |
~/.mowdeck-sign-pass missing or wrong. |
sign-and-install.sh: no iPhone found |
Unlock the phone; pair by USB once; Wi-Fi install needs the same network. |
| iPhone: the top bar can be dragged under the status bar, or the page bounces | A build from before the edge-to-edge layout (ios.contentInset: 'always', page bounce on). Rebuild with build-ios.sh (it runs cap sync, which carries contentInset: 'never' into the Xcode project) and install again. |
| Android: nothing in the background | Expected without ntfy; set up ntfy. Some vendors also kill background apps aggressively (dontkillmyapp.com). |
The configuration, scripts and native code were written against the Capacitor 8.5 sources and are covered by unit tests (the web app's bridge module, the first-launch checks, the APNs channel with a fake Apple endpoint). Building the iOS app, installing it with devicectl, APNs delivery to a phone, the Android Gradle build and the remote-content bridge on real devices are verified on the owner's Mac and phones, not in CI.
Mirrored from docs/ops/mobile-apps.md at commit 94c46ef36cabac4c3a0bacfc0cc0c0af4defdca3. Edit the file in the repository, not this wiki page.