ADR 0140 — Android and Mobile Portfolio Strategy
Status: Proposed — awaiting operator decision on approach (see §Open questions) Date: 2026-07-13 UTC Deciders: software-architect (analysis); operator (final decision) Scope: Raxx mobile portfolio — iOS app, prospective Android app, and the long-term maintenance model for both
Context
Raxx today has:
- Native SwiftUI iOS app (
ios/Raxx/) — 20 Swift files covering passkey auth (PasskeyManager.swift,SessionStore.swift), dashboard, strategies, billing (StoreKitManager.swift), settings, networking, and theme. In TestFlight Wave 2. Stack governed by ADR-0116 (SwiftUI native), ADR-0005 (RP IDraxx.app), ADR-0007 (Apple IAP / StoreKit 2). - Next.js web app (
frontend/raxx-next/) — primary feature surface; extensive route coverage (auth, backtest, dashboard, options, onboarding, beta, etc.). - No Android app.
The operator is considering adding Android. This forces a strategic choice about the mobile portfolio architecture that is effectively irreversible once chosen: the iOS codebase investment, the billing implementations, and the passkey credential registrations all carry migration costs that compound with user growth.
This ADR is a decision-support document. It does not commit to building Android — it defines the fork, the constraints that bear on it, and a recommendation.
Invariants
The following TradeMasterAPI-level invariants constrain every option:
- No stored credentials. Passkeys use public-key challenge-response; no credential value is ever stored server-side or in any mobile app.
- Passkeys / WebAuthn only. No password path. No SMS OTP. WebAuthn RP ID is
raxx.app(ADR-0005) — cross-platform by design and immutable once users register. - Paper-first gating. Live-trading paths require paper-profitable gate. Any Android live-trading surface must enforce this.
- Apple IAP locked for iOS (ADR-0007). Google Play Billing is the Android analog. Webview apps on either store that handle digital goods purchases must use the respective store's billing library — there is no hybrid exception.
- Audit trail for every state change affecting money, permissions, or data access — applies to mobile platforms equally.
- Passkeys fail in in-app webviews. A real Wave 2 beta tester was blocked
when opening the invite link in Gmail (incident
2026-06-12-beta-walkthrough-500.md). Chrome TWA is not an in-app browser and is not affected; Capacitor/WKWebView and Android WebView are affected. - ADR-0116 (SwiftUI native) explicitly rejected Capacitor/webview for iOS on two grounds: App Store guideline 4.7 (thin HTML5 wrapper), and inferior passkey UX from within WKWebView. This precedent directly constrains Option C for iOS.
The Three Options
Option A — Native Both
Keep SwiftUI iOS. Add native Kotlin / Jetpack Compose for Android.
Option B — Cross-Platform Unification
Replace the SwiftUI iOS app with a single cross-platform framework covering both iOS and Android. Sub-options: React Native (B-RN), Flutter (B-FL), or Kotlin Multiplatform (B-KMP).
Option C — Web Wrapper
Wrap the existing Next.js web app in a store-distributable shell. - iOS: already rejected by ADR-0116. - Android: Trusted Web Activity (TWA) or Capacitor (Android only).
Options Matrix
| Dimension | A — Native Both | B-RN — React Native | B-KMP — KMP + SwiftUI/Compose | B-FL — Flutter | C — TWA (Android only) |
|---|---|---|---|---|---|
| iOS investment preserved? | Yes, fully | No — rewrite required | Yes (SwiftUI views stay; shared layer added) | No — rewrite required | Yes |
| Code reuse with Next.js | None (mobile is already separate) | Hooks/utils portable in theory; no component reuse (different renderer) | None (Kotlin shared logic; Next.js is TypeScript) | None | High — same codebase serves web + Android |
| Time-to-Android (eng-weeks) | 8–12 wks | 16–22 wks (iOS rewrite + Android) | 12–16 wks (shared layer + Compose) | 18–24 wks (iOS + Android rewrite, new ecosystem) | 4–6 wks |
| iOS regression risk | None | High — full SwiftUI replacement | Low — iOS views stay | High — full replacement | None |
| Passkeys — iOS | AuthenticationServices (done) |
Community lib; already rejected ADR-0116 | AuthenticationServices (done) |
Community plugin (same risk as RN) | N/A (iOS unchanged) |
| Passkeys — Android | Android Credential Manager API (native, FIDO2, Jetpack) | react-native-passkeys (community) |
Android Credential Manager (native) | passkeys flutter plugin (community) |
Chrome handles WebAuthn natively in TWA (not a webview) |
| Apple IAP | StoreKit 2 (done) | Community RN bridge | StoreKit 2 via Swift interop layer | community plugin | N/A (iOS unchanged) |
| Google Play Billing | Native Play Billing library (Kotlin) | RevenueCat or react-native-purchases |
Native Play Billing (Kotlin) | in_app_purchase pub.dev plugin |
Digital Goods API (Chrome 101+; functional but non-trivial to implement) |
| Maintenance burden (solo) | High — 3 codebases; mobile apps are thin and structurally parallel | Medium — 2 codebases; community-bridge maintenance adds latent risk | Medium — 3 targets, shared logic reduces duplication; build tooling is complex | Medium — 2 codebases; Dart adds a third language | Low — 2 codebases; Android improvements = web improvements |
| App Store quality bar | Full native on both stores | Full native appearance; some bridge jank possible | Full native on both stores | Full native appearance | TWA must pass Lighthouse PWA checks; perceived as web app by many users |
| Solo-founder long-term risk | Two thin native mobile codebases; manageable given structural parallelism | Community libraries on auth + billing critical paths | Complex Gradle + Xcode dual build; KMP v1.9.x still maturing | Dart is a fourth language (TS/Python/Swift + Dart); no reuse with any existing codebase | Binds Android quality to web PWA maturity; Play Billing via Digital Goods API is functional but less proven than native |
Detailed Constraint Analysis
Passkeys
The RP ID raxx.app works natively on Android via the Android Credential Manager
API (Jetpack, API level 28+, covering ~97% of active Android devices). The setup
is analogous to iOS:
- A Digital Asset Links file at
https://raxx.app/.well-known/assetlinks.jsondeclares the Android app package and signing certificate fingerprint. - The Android app calls
CredentialManager.getCredential()— the Jetpack analog to iOSASAuthorizationController. - No bridge, no community library, no WebView. Options A and B-KMP both use this native path.
For Option C (TWA): Chrome on Android handles WebAuthn natively within the TWA
process. This is not the same as an in-app browser. The
feedback_passkey_fails_in_inapp_browsers incident involved Gmail's Chrome Custom
Tab running in a reduced-permission context — TWA runs as a first-class Chrome
activity and has full navigator.credentials access, including passkeys. This is a
genuine passkey-viable path for Android.
For Options B-RN and B-FL: the passkey path routes through a community plugin. ADR-0116 rejected this posture for iOS. The same reasoning applies to Android: a financial app gating live trading behind passkey auth should not depend on a community-maintained bridge to the credential API. If that bridge goes unmaintained, auth breaks.
Billing
iOS (all options that keep iOS native): Apple IAP via StoreKit 2 is locked
(ADR-0007). The existing StoreKitManager.swift is unaffected by the Android
decision.
Android billing by option:
- Option A / B-KMP: Google Play Billing library (Kotlin) — the direct Google-maintained analog to StoreKit 2. Required for Play Store apps selling digital subscriptions.
- Option B-RN / B-FL: RevenueCat or
in_app_purchaseplugin wraps both stores. Production-grade but adds a third-party dependency on the billing critical path and vendor lock-in on subscription state. Not fatal, but a meaningful dependency. - Option C (TWA): The Digital Goods API (
window.getDigitalGoodsService()) is Google's Play Billing bridge for TWAs. It works (used in production by some Google-first apps) but requires the web app to implement a JavaScript billing surface that calls back to the native Play Billing stack. This is not automatic — it requires deliberate implementation work inraxx-nextand a new Raptor endpoint for Play purchase token validation.
Existing iOS Investment Assessment
The iOS app at ios/Raxx/ contains 20 Swift files. The load-bearing integrations:
PasskeyManager.swift— directAuthenticationServicesusage. No equivalent exists in any cross-platform framework without a community bridge.StoreKitManager.swift— StoreKit 2 subscription management. Framework-specific.APIClient.swift,SessionMiddleware.swift,Endpoint.swift— networking layer. Most portable layer; analogous code will exist in any Android implementation regardless of option chosen.- SwiftUI views (~10 files) — render iOS-specific UI; thrown away under B-RN and B-FL, preserved under A and B-KMP.
TestFlight Wave 2 is the specific sunk-cost signal: this is not a prototype, it is a beta-validated app. Replacing it with a community-bridge framework resets beta stability.
Next.js Code Reuse
React components do not run in React Native (different renderer). No component
code from raxx-next is mechanically portable to React Native, Flutter, or KMP.
Business logic patterns (API call structure, state shape, route guards) inform
mobile implementations but cannot be shared.
The exception is Option C (TWA): the Android app is raxx-next. Every
feature shipped to the web app is simultaneously available on Android. This is the
genuine, non-trivial advantage of the wrapper approach — for a solo founder with a
large web feature surface, it eliminates the "web feature, then mobile feature"
duplication.
Maintenance Burden (Solo Founder)
Ranked from lightest to heaviest ongoing maintenance:
- Option C (TWA, Android only): 2 active codebases (web + iOS). Android improvements are subsumed into web work. Lowest friction.
- Option B-RN: 2 active codebases (web + React Native). But bridge risk on auth + billing paths.
- Option A: 3 codebases. However, the mobile apps are thin consumption surfaces — the structural parallelism between iOS and Android (same API, same Raptor endpoints, same auth flow) makes the duplication mechanical rather than complex. Once Android v1 exists, maintaining feature parity is modest overhead.
- Option B-KMP: 3 build targets with a shared Kotlin layer. Build complexity is the overhead; logic sharing is the benefit. At the current 20-file scale, the benefit is marginal.
- Option B-FL: Web + Flutter = 2 codebases, but Dart is a fourth language (TypeScript/Python/Swift/Dart). Context-switching cost is underestimated by the headline "2 codebases."
Recommendation
Option A — keep SwiftUI iOS, add native Kotlin/Jetpack Compose Android — deferred to post-launch.
Why not B-RN or B-FL
Both require throwing away PasskeyManager.swift and StoreKitManager.swift —
the two most load-bearing, framework-specific integrations in the iOS app —
in exchange for community bridges. ADR-0116 already made this call for iOS. The
TestFlight Wave 2 investment is real. Resetting it carries regression risk on auth
and billing, which are the paths where a financial app least tolerates surprise.
Why not B-KMP
KMP preserves iOS views and native auth/billing on both platforms. The objection is timing and marginal value: at 20 Swift files, the shared-logic benefit is modest, and Gradle + Xcode dual-build toolchain complexity is non-trivial to set up correctly (particularly with Swift package interop). KMP is worth revisiting when the codebase is larger and the team is more than one.
Why not Option C (TWA) as the long-term answer
A Trusted Web Activity on Android gives Android users "the website in Chrome." For most apps this is a reasonable trade. For Raxx — a financial app whose brand is structure, discipline, and trust — delivering a native experience on iOS and a web-wrapper experience on Android creates a quality asymmetry that contradicts the brand. Haptic feedback, native navigation gestures, APNs-quality push notifications, and a Keychain-equivalent credential store (Android Keystore) are available in a native app and not in a TWA. The first Android users are likely to compare.
If Option C is used: it should be explicitly scoped as a bridge strategy with a documented upgrade path to native Android, not a permanent answer.
Why defer
Pre-revenue, with iOS in TestFlight Wave 2, the marginal return on Android investment is lower than shipping iOS to production and validating revenue. US iOS market share (~55%) covers the early-adopter demographic for a financial app. The 8–12 weeks of Android native work has a better expected return after the first revenue signal validates the iOS model.
The single decision the operator must make
Confirm the iOS architecture is permanent (Option A) or authorize a cross-platform rewrite (Option B).
- If A: iOS readiness plan proceeds. Android is a future sprint, sub-cards filed when the operator gives the go signal.
- If B: iOS readiness plan pauses immediately.
product-managermust be notified before the next sprint. - If C (TWA bridge, fast Android): iOS readiness plan proceeds. TWA sub-cards are filed alongside iOS work. Operator must explicitly accept the quality asymmetry and document the upgrade intent.
If the operator needs Android fast
Option C (TWA, Android only) gets Android distribution in 4–6 weeks without touching the iOS codebase. This requires:
- PWA baseline in
raxx-next(service worker + web manifest, Lighthouse ≥ 90). bubblewrap-generated APK from the manifest.- Digital Goods API implementation for Play Billing (2–3 additional weeks).
assetlinks.jsonhosted atraxx.appfor passkey binding.
This is the fastest credible path. It is not the highest-quality path.
iOS Readiness Plan Coupling
This ADR directly gates the concurrent iOS readiness plan:
- Option A confirmed: iOS readiness plan proceeds as scoped. SwiftUI, StoreKit 2,
AuthenticationServicesare confirmed as the lasting iOS architecture. Android is additive in a future sprint. - Option B authorized: iOS readiness plan should pause immediately. Investing
further in SwiftUI-specific features (widget extensions, APNs rich notifications,
etc.) while planning a React Native or Flutter rewrite wastes those weeks. The
product-managerdrafting the iOS plan must be notified before the next sprint. - Option C (TWA bridge) selected: iOS readiness plan continues unaffected. TWA work is parallel and isolated.
Digital Asset Links (Android prerequisite, any Android option)
Any Android app using passkeys on raxx.app requires a Digital Asset Links file:
GET https://raxx.app/.well-known/assetlinks.json
Content-Type: application/json
[{
"relation": ["delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "<tbd — open question #3>",
"sha256_cert_fingerprints": ["<tbd — release signing key fingerprint>"]
}
}]
This file coexists with the existing iOS AASA file at the same origin. It is a Raptor / hosting task (one static file), not an Android codebase task. It must be in place before any Android passkey registration is attempted, including in staging.
Schema Impact
No schema changes required for this ADR itself. The subscription_source column
planned in ADR-0007 ("web" / "ios" / "android") is the only schema addition
needed when Android billing is implemented. No migration until Android billing is
built.
Rollout Plan (Option A — recommended)
- Now — pre-Android: Ship iOS to App Store production. No Android work.
- Post-launch spike (~2 weeks): Android native feasibility spike. Proof-of-concept: Android Credential Manager passkey registration against staging + Google Play Billing sandbox purchase. This is sub-card #1.
- Android v1 build (~8–12 weeks): Feature-parity with iOS app (auth, dashboard, strategies, billing). No live-trading path until paper-first gate is confirmed on Android.
- Play Store beta: Mirror TestFlight model.
- GA: Requires Android live-trading paper-first gate confirmation.
Security / GDPR Checklist
- PII collected: Device push token (optional, for notifications); subscription state. Same surface as iOS — no new PII categories introduced by this ADR.
- Retention period: Push token retained until DSR or app uninstall callback. Subscription state per ADR-0003 retention schedule.
- Deletion on DSR: Raptor
/api/gdpr/erasedrops push token + subscription records. Android uninstall callback triggers the same erasure path. - Audit trail: All subscription state changes logged to audit trail. Play Billing webhook events → audit trail. Paper-first gate status changes → audit trail.
- Stored credentials: None. Android Keystore holds passkey private key on device; Raptor holds only the public key. No-stored-credentials invariant upheld.
- Breach notification: No change from existing posture. Android adds no new credential surface.
- Secrets location + rotation: Google Play Billing service account key in Infisical (same pattern as Apple App Store server key). Rotatable without redeploy.
- Kill-switch: Paper-first gate governs live trading on Android same as iOS and web. Play Console allows rolling back or suspending Android distribution independently of iOS. Server-side session invalidation revokes access on any platform immediately.
Open Questions (require operator decision before any sub-cards can be claimed)
-
THE core decision: Option A (native iOS + native Android, deferred)? Option C (TWA bridge, fast Android)? Option B (cross-platform unification)? Everything downstream — iOS readiness plan continuation, Android sub-cards, billing backend — depends on this answer.
-
Android launch timing: Co-launch with iOS? First 90 days post-iOS launch? Later? If "at iOS launch," Option A is not achievable (insufficient time); the only candidates are Option C or a pre-decision to begin Android native immediately.
-
Android package name: Must be chosen before the Digital Asset Links file can be committed. Suggested:
app.raxx.android— operator confirms. -
Google Play Developer Account: Is one already open under MooseQuest LLC? If not, the 48-hour registration wait is on the Android critical path.
-
Cross-billing upgrade path (carry-over from ADR-0007): A user who subscribes on web (Stripe), then installs the Android app — what do they see? This interaction pattern must be decided before Android billing is implemented.
Revisit When
- iOS reaches App Store production and Android demand is evidenced by user feedback or waitlist data.
- Revenue signal on iOS justifies the 8–12 weeks of Android native investment.
- A team hire changes the solo-founder maintenance calculus.
- KMP tooling reaches v2.x stable (currently v1.9.x — worth reassessing at that milestone).
- Google Play TWA + Digital Goods API reaches broader production adoption (reassess Option C quality gap annually).