ADR-009: Native Mobile Application Strategy
ADR-009: Native Mobile Application Strategy
Status
Proposed — Pending engineering team review
Context
The platform currently serves mobile users through Ionic (peeq-mono) — a web-based hybrid approach that wraps the Angular SPA in a Capacitor shell for iOS and Android. The user experience is functionally a web browser in a native wrapper. The business wants a truly native mobile experience alongside the web platform, with dedicated iOS and Android apps.
Current Mobile State
| Aspect | Current |
|---|---|
| Framework | Ionic 6 + Capacitor 3.6 (inside peeq-mono Angular Nx monorepo) |
| Architecture | Angular web app wrapped in native shell |
| Platforms | iOS + Android (via Capacitor) |
| Code sharing | 100% shared with web (same Angular components) |
| Native APIs | Capacitor plugins for camera, push, biometrics |
| Performance | Web-view rendering — no native UI components |
| Offline | No offline support observed |
| App Store | Unclear if currently published |
| Testing | Near-zero (same as web) |
Why Go Native
| Limitation of Ionic/Capacitor | Native Advantage |
|---|---|
| Web-view rendering feels sluggish | Native UI components render at 60fps |
| No native navigation (gestures, transitions) | Platform-native navigation patterns |
| Limited push notification control | Full APNs/FCM integration |
| Web-view video playback issues | Native video player with PiP, Chromecast |
| No offline-first architecture | SQLite/Realm for offline data |
| App Store review friction (web-view apps) | Apple prefers native rendering |
| Limited deep linking | Universal Links (iOS) / App Links (Android) |
| No widgets or watch extensions | Full platform integration |
Decision
Build native mobile apps using React Native with shared business logic between web (Next.js, per ADR-008) and mobile. The shared layer includes TypeScript types, GraphQL operations, authentication logic, and business rules. The UI layer is platform-specific (React for web, React Native for mobile).
Architecture: Shared Monorepo
graph TB
subgraph "Shared Packages (TypeScript)"
TYPES[types<br/>Domain models, API types]
GQL[graphql<br/>Operations, fragments, hooks]
AUTH[auth<br/>Keycloak client, token mgmt]
LOGIC[business-logic<br/>Validation, formatting, rules]
end
subgraph "Web (Next.js)"
WEB_APP[Next.js App Router]
WEB_UI[React Components<br/>Tailwind CSS]
end
subgraph "Mobile (React Native)"
RN_APP[React Native App]
RN_UI[Native Components<br/>React Native Paper / Tamagui]
IOS[iOS Build<br/>Xcode / EAS]
AND[Android Build<br/>Gradle / EAS]
end
subgraph "Backend (Unchanged)"
API[24+ GraphQL Gateways]
KC[Keycloak]
end
TYPES --> WEB_APP
TYPES --> RN_APP
GQL --> WEB_APP
GQL --> RN_APP
AUTH --> WEB_APP
AUTH --> RN_APP
LOGIC --> WEB_APP
LOGIC --> RN_APP
WEB_APP --> WEB_UI
RN_APP --> RN_UI
RN_UI --> IOS
RN_UI --> AND
WEB_APP -->|GraphQL| API
RN_APP -->|GraphQL| API
WEB_APP -->|OAuth2| KC
RN_APP -->|OAuth2| KC
Shared Code Estimation
| Layer | Web | Mobile | Shared |
|---|---|---|---|
| TypeScript types | Uses | Uses | 100% shared |
| GraphQL operations | Apollo Client | Apollo Client (RN) | 95% shared (hooks differ slightly) |
| Auth logic | next-auth + Keycloak | expo-auth-session + Keycloak | 80% shared (transport differs) |
| Business logic | Validation, formatting | Same | 100% shared |
| UI components | React + Tailwind | React Native Paper | 0% shared (different renderers) |
| Navigation | Next.js Router | React Navigation | 0% shared |
| Platform APIs | Browser APIs | Expo modules | 0% shared |
Estimated shared code: 40-50% of non-UI codebase. This is significant — it means GraphQL queries, type definitions, validation rules, and auth flows are written once.
React Native Technology Stack
| Component | Choice | Rationale |
|---|---|---|
| Framework | React Native + Expo | Managed workflow, OTA updates, EAS Build |
| Navigation | React Navigation 7 | Industry standard for RN navigation |
| State | Zustand or Jotai | Lightweight, works with GraphQL cache |
| GraphQL | Apollo Client for React Native | Same client as web (ADR-008) |
| Auth | expo-auth-session + Keycloak | Keycloak PKCE flow for mobile |
| Video | react-native-video + Mux SDK | Native Mux player for iOS/Android |
| Chat | Stream Chat React Native SDK | Stream provides official RN SDK |
| Push | expo-notifications + FCM/APNs | Managed push through Expo |
| Storage | expo-secure-store (tokens) + MMKV (cache) | Secure + fast local storage |
| Styling | Tamagui or React Native Paper | Cross-platform component library |
| Build | EAS Build (Expo) | Cloud builds for iOS + Android |
| OTA updates | EAS Update | Ship JS updates without App Store review |
| Testing | Detox (E2E) + Jest (unit) | Native E2E testing |
Feature Prioritization (Mobile App)
MVP (Phase 1)
| Feature | Priority | Complexity |
|---|---|---|
| Keycloak authentication (Magic Link) | P0 | M — PKCE flow + Magic Link deep link |
| Expert profile browsing | P0 | S — GraphQL + native list/detail |
| Content viewing (articles, videos) | P0 | M — Mux native player integration |
| Subscription management | P0 | M — Stripe mobile SDK |
| Push notifications | P0 | S — Expo notifications |
| Follow/unfollow experts | P0 | S — GraphQL mutation |
| Deep linking (shared profiles/content) | P0 | M — Universal Links / App Links |
Phase 2
| Feature | Priority | Complexity |
|---|---|---|
| Live streaming (Mux) | P1 | M — native player with live support |
| In-app chat (Stream) | P1 | M — Stream RN SDK |
| Shoutout purchase + viewing | P1 | M — payment flow + video |
| Event browsing + RSVP | P1 | S — GraphQL queries |
| Offline content (saved articles/videos) | P1 | L — local storage + sync |
Phase 3
| Feature | Priority | Complexity |
|---|---|---|
| Webinar registration + join | P2 | M — Zoom SDK integration |
| Class catalog browsing | P2 | S — GraphQL |
| Message board | P2 | S — SSE for real-time |
| Widgets (iOS) / shortcuts (Android) | P2 | M — platform-specific |
| Biometric auth | P2 | S — Expo LocalAuthentication |
| Apple/Google Pay | P2 | M — Stripe mobile payment sheet |
Keycloak Mobile Auth Flow
sequenceDiagram
participant App as React Native App
participant Browser as System Browser
participant KC as Keycloak
participant API as Backend API
App->>Browser: Open Keycloak authorize URL (PKCE)
Browser->>KC: Login page (Magic Link or password)
KC->>Browser: Authorization code (redirect)
Browser->>App: Deep link with auth code
App->>KC: Exchange code for tokens (PKCE verifier)
KC->>App: Access token + refresh token
App->>App: Store tokens in SecureStore
App->>API: GraphQL requests with Bearer token
API->>App: Responses
Magic Link on mobile: When user requests Magic Link,
email/SMS contains a deep link
(theagilenetwork://auth/magic?token=xxx) that opens the app
directly with the auth token. Requires Universal Links
configuration.
Hypothesis Background
Primary: React Native with Expo provides a native mobile experience while sharing 40-50% of code with the Next.js web app, delivering better user experience than the current Ionic wrapper.
- Evidence: Ionic/Capacitor renders in a web-view — users experience web-like performance, not native. React Native renders actual native UI components.
- Evidence: ADR-008 proposes Next.js (React) for web. React + React Native enables the shared monorepo pattern. Angular + separate native (Swift/Kotlin) would have zero code sharing.
- Evidence: The platform’s key mobile features (video playback, chat, payments, push notifications) all have mature React Native SDKs from their respective providers (Mux, Stream Chat, Stripe, Expo).
Alternative 1: Keep Ionic/Capacitor. - Rejected: Ionic provides a mobile presence but not a native experience. Apple has been tightening App Store policies around web-view apps. The platform’s content-rich, video-heavy nature demands native performance.
Alternative 2: Build fully native (Swift for iOS, Kotlin for Android). - Rejected: Requires two separate codebases with zero code sharing. Double the development and maintenance effort. The team would need iOS (Swift/UIKit or SwiftUI) and Android (Kotlin/Jetpack Compose) specialists. React Native achieves 90%+ native performance with a single codebase.
Alternative 3: Flutter. - Rejected: Flutter uses Dart, not JavaScript/TypeScript. Zero code sharing with the Next.js web app. While Flutter produces excellent native UIs, the lack of shared code with the web platform is a dealbreaker for a small team.
Alternative 4: Kotlin Multiplatform (KMM). - Rejected: KMM shares business logic (Kotlin) across iOS/Android but doesn’t help with web (TypeScript). The shared layer would be in Kotlin (mobile) and TypeScript (web) — no actual sharing. Also, the team’s backend is Java/Spring Boot, not Kotlin.
Falsifiability Criteria
- If React Native + Expo cannot integrate with Keycloak Magic Link via deep links → evaluate alternative auth flows or custom native modules
- If Mux React Native SDK provides unacceptable video playback quality compared to native → build custom native video module
- If App Store review rejects the app for any Expo-related reasons → switch to bare React Native workflow
- If shared code between web and mobile is <25% in practice → the monorepo benefit doesn’t justify the architectural complexity; consider separate mobile codebase
- If React Native performance (60fps scrolling, <100ms touch response) doesn’t meet expectations → evaluate native Swift/Kotlin for performance-critical screens
Evidence Quality
| Evidence | Assurance |
|---|---|
| Current app is Ionic/Capacitor web-view | L2 (verified — peeq-mono/mobile uses Ionic 6 + Capacitor 3.6) |
| React Native renders native components | L2 (framework documentation, industry adoption) |
| Mux has React Native SDK | L1 (Mux documentation) |
| Stream Chat has React Native SDK | L2 (Stream documentation, actively maintained) |
| Stripe has React Native SDK | L2 (Stripe documentation, @stripe/stripe-react-native) |
| Expo supports Keycloak PKCE | L1 (expo-auth-session documentation) |
| Shared code estimate (40-50%) | L0 (estimate based on architecture; needs implementation to verify) |
| App Store acceptance of React Native | L2 (thousands of published RN apps including major brands) |
| Current Ionic app performance benchmarks | L0 (no measurement data) |
Overall: L1 (WLNK capped by shared code estimate L0 and current app performance data L0)
Bounded Validity
- Scope: Fan-facing mobile experience (iOS + Android). Expert and admin functionality remain web-only initially. Expert mobile app is Phase 3 consideration.
- Expiry: Re-evaluate if React Native introduces breaking architectural changes, or if Kotlin Multiplatform matures to support web targets effectively.
- Review trigger: If Expo SDK limitations block critical features (specific native modules not available), or if Apple/Google platform APIs move faster than React Native ecosystem can adapt.
- Monitoring: App Store ratings, crash-free rate (>99.5%), cold start time (<2s), video playback quality metrics, user engagement compared to web.
Consequences
Positive: - True native mobile experience (60fps, native navigation, platform-standard UX) - 40-50% code sharing with web via shared monorepo - Consistent GraphQL API usage across web and mobile - OTA updates via EAS (ship fixes without App Store review) - Access to all native APIs (biometrics, push, camera, PiP video) - Deep linking for marketing and sharing - Single team can maintain web + mobile (React skills transfer)
Negative: - New codebase to build and maintain (even with code sharing) - Expo managed workflow has some native module limitations (escape hatch: bare workflow) - App Store review process adds deployment friction - Need to implement Keycloak Magic Link deep linking - Native video player integration requires testing across devices - Must maintain App Store listings, screenshots, descriptions
Mitigated by: Expo simplifies native builds and OTA updates. Stream Chat, Mux, and Stripe all provide maintained React Native SDKs — no custom native bridges needed for core features. Shared monorepo means business logic changes automatically propagate to mobile.
Decision date: 2026-01-31 Review by: 2026-07-31