ADR

ADR-009: Native Mobile Application Strategy

Last updated: 2026-02-01 | Decisions

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.

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

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

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