PWA vs APK vs Tauri mobile — the ranked path to installable · content/phone-install-paths.md
Phone Install Paths for the QALS PWA (Tailscale-only, plain HTTP)
Researched: 2026-09-15. Sources verified via web fetch (DuckDuckGo HTML + primary docs). Items I could not confirm live are tagged [UNVERIFIED].
Our situation: PWA at http://100.73.134.20:8837/phone — a Tailscale CGNAT IP, no domain, no TLS. This cannot be installed as a PWA today. This doc maps the escape routes.
Ranked recommendations
| Rank | Path | Speed to ship | UX quality | Security | Maintenance | Notes |
|---|---|---|---|---|---|---|
| 1 | tailscale serve HTTPS on *.ts.net + real PWA install |
★★★★★ (hours) | Good (WebAPK on Android, A2HS on iOS) | Strong (real Let's Encrypt cert, tailnet-gated) | Trivial (auto-renew) | No public domain needed. Unlocks everything else. |
| 2 | Plain APK: WebView wrapper + manual Tailscale app | ★★★ (days) | Native-ish icon/offline shell | Good (own signing key) | Moderate (2 surfaces to update) | Also needs path 1 anyway for service worker; keep UI single-sourced. |
| 3 | Private F-Droid repo (distribution layer for #2) | ★★★ | N/A (channel) | Good (signed index) | Low-moderate | Pairs with any APK; voucher page lists repo URL + fingerprint. |
| 4 | Tauri 2 Android build of existing desktop app | ★★ (weeks) | Best (native shell, deep links, QR plugin) | Good | Heavier (Rust+Kotlin+Gradle toolchain) | Phase-2 upgrade once UX is proven. |
| 5 | Play internal/private track | ★★ (account + review) | Store-grade installs/updates | Strong | Low, but Google-gated | 100 testers internal, org-targeted private apps; consider later. |
1. PWA install requirements 2025-26, and what blocks our HTTP URL
Verified (OpenPWA installability criteria, fetched 2026-09-15; MDN "Making PWAs installable"; MDN "Secure contexts", modified 2026-09-14):
- HTTPS is a hard gate. A document is a secure context if served over
https, or from a loopback origin:localhost,*.localhost,127.0.0.0/8,::1/128(MDN Secure Contexts).file://also qualifies. - Manifest minimums:
name/short_name,start_url,display: standalone|fullscreen|minimal-ui, icons at 192×192 and 512×512 (maskable recommended). - Chrome no longer hard-requires a service worker for
beforeinstallprompt— but some Chromium variants (Samsung Internet) still do, and Lighthouse audits want one (OpenPWA, 2026). Register one anyway; it carries offline + push. - Chrome/Android installs produce a WebAPK (thin native wrapper, Play-signed); iOS produces a web clip via manual Share → "Add to Home Screen" — no
beforeinstallpromptexists on iOS, ever. iOS web push works only for installed home-screen apps (Safari 16.4+). Firefox Android supports PWA install from 128+. - Chrome also applies user-engagement heuristics before firing the install prompt; capture
beforeinstallpromptand callprompt()from a button.
The blocker, precisely: http://100.73.134.20:8837 is neither HTTPS nor loopback → window.isSecureContext === false → service worker registration is refused, beforeinstallprompt never fires, and no browser (Chrome, Safari, Samsung, Firefox) will offer install. Clipboard, notifications, and most modern APIs are also gated. The Tailscale tunnel is encrypted, but browsers can't see that — they only trust TLS + trusted roots.
2. Workarounds for plain-HTTP Tailscale URLs — and the real fix
chrome://flags/#unsafely-treat-insecure-origin-as-secure— per-device flag listing specific http origins; historically enables SW + install. Must be hand-set on every phone, invisible to users, resets on flag removal. Dev-only. [UNVERIFIED — could not fetch Chrome docs; flag existence is well-known]- Self-signed cert — Android Chrome requires installing a user CA (settings trip + warnings for user-trusted CAs on newer Android); iOS is harsher (manual profile install). Fragile, per-device, and still fails the "trusted root" bar for some APIs. Not recommended.
- localhost trick — browsing
http://localhost:8837on the server itself is secure-context, but that's irrelevant for phones. - ✅ The real fix: Tailscale's own HTTPS. Verified (tailscale.com/kb/1153/enabling-https, validated Dec 10 2025): enable MagicDNS + HTTPS in the admin console, then
tailscale certprovisions a real Let's Encrypt certificate formachine.tailNNNN.ts.netvia DNS-01 — no public domain, no open ports. Certs auto-renew when served through the Caddy integration ortailscale serve[UNVERIFIED for serve specifics — not fetched]. Cost: your machine hostname is published in the Certificate Transparency ledger (opaque tailnet name mitigates). Result:https://qals.tailNNNN.ts.net/phoneis a first-class installable PWA on every platform, still reachable only inside the tailnet.
3. Tauri 2 mobile (Android/iOS)
Verified (v2.tauri.app, fetched 2026-09-15):
- Tauri 2.0 went stable 2024-10-02 with iOS + Android as first-class targets (same Rust core; Kotlin/Swift shells); steady 2.x line since — v2.11.x by mid-2026 (tauri.app/release/tauri). Mature enough to bet on for an internal tool.
- Android delta from our desktop app: install Android Studio + SDK Platform, Platform-Tools, NDK, Build-Tools, cmdline-tools; set
ANDROID_HOME/NDK_HOME;rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android; thentauri android init/tauri android build. iOS requires macOS + Xcode + CocoaPods. - Wry on Android uses the system WebView (Chromium-based, Chrome-autoupdated), not bundled WebKit — so rendering matches Chrome-on-Android; iOS uses WKWebView. Porting is mostly: move sidecar binaries (don't assume desktop binaries run — need
aarch64-linux-androidbuilds), drop desktop-only plugins (tray, global-shortcut, autostart), keep UI remote or bundled. - Relevant plugins verified in the catalog: Deep-Linking (custom URL schemes on all platforms), Barcode Scanner, Biometric, Notifications — exactly the voucher-QR toolkit.
- Caveat: cell/Rust networking must use Android's VpnService to be a VPN; Tauri doesn't provide that. Embedded Tailscale = fork territory (below).
4. Android APK sideloading for a private-network app
Pieces (standard Android tooling; developer.android.com unreachable during research — specifics tagged):
- Build: Android Studio GUI, or CLI-only:
gradle assembleReleasewith command-line tools; Tauri projects can stay CLI. A WebView wrapper app is ~1 Activity +WebView.loadUrl(...); service worker needs the HTTPS origin (§2). - Signing: generate a keystore once (
keytool -genkeypair), sign withapksigner; keep the key — same key = updates install over old versions. [UNVERIFIED command details] - Minimums: typical
minSdk 26(Android 8),targetSdk 35to satisfy current Play policy if you ever publish. [UNVERIFIED exact policy year] - Tailscale on Android: no official embeddable SDK. Two integration levels:
1. Intent automation — Tailscale's Android app responds to
com.tailscale.ipn.CONNECT/ STOP intents (usable from adb/Tasker/your app). [UNVERIFIED — DDG captcha-blocked; test viaadb shell am start -a com.tailscale.ipn.CONNECT] 2. Fork — tailscale-android is open source; embedding the VPN into your own app means forking the VpnService plumbing (heavy, GPL considerations). Not worth it for one internal app. - Distribution mechanics: host the signed APK at a URL (can be tailnet-internal if the installer's phone is already on the tailnet — but see §6 QR caveat), or F-Droid repo, or Play internal track.
5. How other projects distribute without stores
- Briar (verified, briarproject.org/download-briar, 2026-09-15): simultaneously on Google Play, F-Droid, and direct APK from their own site — with PGP signatures and reproducible builds for verifiability. The canonical pattern: many channels, one signature.
- Signal: direct APK downloads from signal.org (GPG-signed releases) long predated/predate Play dependency; QR-based device linking via Settings → Linked Devices. [UNVERIFIED — support.signal.org returned 403; QR encodes a
tsdevice:/link + key material, from memory] - F-Droid private repos:
fdroidserverlets you run your own repo (signedindex-v1.json, APKs behind any URL); users add it in the F-Droid client via repo URL + fingerprint — no public store listing. [UNVERIFIED specifics — docs page not fetched] - WhatsApp Business direct APK / other direct-APK vendors: common in APK-direct markets; no special pattern beyond signing + hosting. [UNVERIFIED]
- Private Play Store track (verified, support.google.com/googleplay/android-developer/answer/9845334, fetched 2026-09-15): internal testing track = up to 100 testers, builds live "within minutes", no full review for internal; Managed Google Play private apps target an organization ID and are not searchable publicly (answer/9874937). Requires a Play developer account ($25) and Google-account testers. Good later-stage option; heavyweight for "Alexei + a few phones."
6. QR-code install flow and qals:// deep links
- Signal-style QR linking: desktop shows QR containing a one-time URL+keys; phone app scans and pairs. For install QRs the equivalent is: QR encodes an HTTPS URL hosting the APK or Play/F-Droid link — the phone's camera/browser resolves it and Android offers the sideload/install consent flow.
- ⚠️ QALS-specific catch: a QR pointing at our tailnet-only voucher page will not resolve on a fresh phone — the phone isn't in the tailnet until Tailscale is installed + authenticated. So: install QR must hit a publicly-resolvable URL (Play listing, public APK host, or F-Droid repo page), while the voucher/connect QR (
qals://voucher/VCH-XXXX) is scanned from inside the already-installed app and works offline-tailnet. - Custom schemes: Android = intent filter
<data android:scheme="qals"/>; iOS =CFBundleURLTypes. No domain verification needed for bare schemes (OS may show a chooser). Verified Tauri ships a Deep-Linking plugin handling this cross-platform. Verdict:qals://connect/...is a win, but only for the native/Tauri app — PWAs on Android can advertiseprotocol_handlersin the manifest [spotty support — UNVERIFIED on current Chrome Android behavior], and iOS web clips can't register schemes at all. Keephttps://…/voucher/VCH-XXXXas the universal fallback that any browser can open once the phone is on the tailnet.
7. QALS fit — phased plan
- Now (hours): Enable MagicDNS + HTTPS;
tailscale servethe phone app athttps://<host>.<tailnet>.ts.net/phone. Add manifest + icons + service worker. → Installable PWA everywhere, zero new infrastructure, no public exposure (tailnet still gates access). Publish the voucher page on the same HTTPS origin so post-install QR flows work. - Next (days): If the HyperOS/PWA friction (your Redmi auto-denies adb/PWA oddities — see AGENT_GOTCHAS) annoys anyone, wrap the same UI in a ~1-screen WebView APK, signed once, distributed by QR → public URL. Point it at the ts.net HTTPS origin.
- Later (weeks, only if justified): Tauri 2 Android port of the existing desktop app for deep links (
qals://voucher/...), QR scanning in-app, biometric unlock — using the Deep-Linking + Barcode-Scanner plugins. F-Droid private repo or Play private track as the channel when the audience grows past sideloading-by-hand.
Key sources: MDN Secure Contexts (2026-09-14); OpenPWA Installability Criteria (fetched 2026-09-15); tailscale.com/kb/1153 (validated 2025-12-10); v2.tauri.app Prerequisites + plugin catalog (updated 2026-08); Play Console Help 9845334 (2026); briarproject.org/download-briar (2026-09-15).