Skip to content

Mobile Architecture — React Native (Expo) Monorepo Integration

Status: Deferred (design complete, implement post-seed) Date: 2026-04-17 Session: 41 Decision: React Native with Expo, integrated into frontend monorepo


Decision: React Native (Expo) over SwiftUI or Flutter

Why not SwiftUI

  • iOS only — Android would need a separate Kotlin/Jetpack Compose codebase
  • Zero code sharing with existing React/TypeScript web apps
  • No official Clerk Swift SDK — custom JWT handling needed
  • Maintaining Swift + Kotlin + TypeScript = three languages for a solo developer
  • SwiftUI's native performance advantage is irrelevant for a chat + document upload app

Why not Flutter

  • Dart is a separate language — zero code sharing with TypeScript stack
  • No official Clerk Flutter SDK
  • Strong choice if starting from scratch, but Curaway has an existing React/TypeScript monorepo

Why React Native (Expo)

  • Same language (TypeScript) as entire stack
  • 40-60% code sharing with web apps via shared-core/
  • Official @clerk/clerk-expo SDK
  • Slots into monorepo as packages/mobile-patient/
  • Expo EAS: cloud builds (no Xcode/Android Studio for most development), OTA JS updates without App Store review
  • AI-assisted development (Claude Code, Cursor) works natively with TypeScript
  • Near-native performance — uses real iOS UIKit and Android Views under the hood
  • Single codebase → iOS + Android

When SwiftUI might revisit

If Curaway integrates Apple Health (patient vitals import) or Apple Wallet (appointment cards, boarding passes), a thin Swift native module handles those specific APIs. Expo supports native modules for this — the rest of the app stays React Native.


Monorepo Structure

frontend/
  packages/
    shared-core/           ← Types, API client, business logic (pure TypeScript)
    shared-web/            ← Web UI components (React + DOM)
    shared-native/         ← React Native UI components (future)
    patient-app/           ← curaway.ai (web)
    coordinator-app/       ← coordinator.curaway.ai (web)
    provider-app/          ← provider.curaway.ai (web, Phase 1)
    mobile-patient/        ← React Native Expo (this spec)
    mobile-coordinator/    ← React Native Expo (if field use warrants)

shared-core/ (platform-agnostic)

Pure TypeScript — no DOM, no React Native. Imports work on both web and native.

Module Contents
types/ Patient, Case, Layer, PFS/HSS/FMS, Transport, Booking, Provider types
api/ API client (fetch-based), endpoint definitions, response types
auth/ Clerk token management hooks (platform-agnostic logic)
scoring/ PFS/HSS/FMS display formatters, threshold labels, color mappings
validation/ Form validation rules, file upload validation logic
formatting/ Date/currency/locale formatters
constants/ Feature flag keys, event names, error codes

shared-web/ (web only)

React components using DOM elements (div, button, span). Used by patient-app, coordinator-app, provider-app.

shared-native/ (React Native only)

React Native equivalents of shared-web components. Created when mobile ships.

Web (shared-web) Native (shared-native) Effort
<div> <View> Trivial
<span>, <p> <Text> Trivial
<button> <Pressable> Trivial
CSS/Tailwind NativeWind (Tailwind for RN) Low
<input> <TextInput> Low
EventSource (SSE) react-native-sse or polyfill Low
File upload (drag & drop) expo-document-picker + expo-image-picker Low
Web Speech API expo-speech or Whisper API Low
Slide panel (EHR drawer) react-native-reanimated bottom sheet Medium
PDF viewer react-native-pdf Medium

Migration path from today

Step 1: Initialize pnpm workspace in frontend/ (curaway-frontend/)
Step 2: Extract shared types + API client → packages/shared-core/
Step 3: Move patient app → packages/patient-app/
Step 4: Create packages/shared-web/ (extract reusable web components)
Step 5: Create packages/mobile-patient/ (Expo app)
Step 6: Create packages/shared-native/ (RN equivalents of shared-web)

Steps 1-4 are prerequisites (monorepo setup). Steps 5-6 are the mobile build.


Mobile Patient App — Feature Scope

P0 — Core experience (launch)

Feature Description Shared from
Authentication Clerk sign-in/sign-up, biometric unlock shared-core (auth logic) + @clerk/clerk-expo
Case list View existing cases, create new shared-core (types, API)
Chat interface Full conversational intake with rich cards shared-core (types) + new native chat components
Document upload Camera capture + file picker + upload to R2 expo-camera + expo-document-picker + shared-core (API)
Progress tracking Layer completion rings, step indicators shared-core (scoring formatters)
Transport panel Transport bookings, journey timeline, driver details shared-core (types)
Push notifications Booking confirmations, driver alerts, coordinator messages expo-notifications (APNs + FCM)
Summary view Layer summary panel (About You, Health Profile, etc.) shared-core (types, formatters)

P1 — Enhanced (post-launch)

Feature Description
Offline support Queue messages and uploads when offline, sync when connected
Document scanner ML-enhanced document scanning with edge detection
Voice input Whisper API transcription (same as web)
Provider comparison Side-by-side provider cards with HSS scores
Coordinator chat Human-to-human messaging with coordinator
Biometric auth Face ID / Touch ID for sensitive data access

P2 — Extended

Feature Description
Deep links curaway://case/123 opens specific case
Apple Health import Import vitals via HealthKit (Swift native module)
Appointment wallet pass Apple Wallet / Google Pay pass for hospital appointment
Recovery tracking Daily check-in, pain scoring, milestone tracking

Technical Architecture

Expo configuration

{
  "expo": {
    "name": "Curaway",
    "slug": "curaway",
    "version": "1.0.0",
    "platforms": ["ios", "android"],
    "plugins": [
      "expo-camera",
      "expo-document-picker",
      "expo-notifications",
      "expo-local-authentication",
      "expo-secure-store"
    ]
  }
}

Auth flow

App launch
  → Check SecureStore for Clerk session token
  → Valid → Load case list
  → Expired/missing → Clerk sign-in screen (@clerk/clerk-expo)
  → Optional: biometric unlock (expo-local-authentication) for returning users

Chat architecture

Same API (POST /api/v1/cases/{id}/chat) as web. Differences:

Concern Web Mobile
SSE streaming EventSource (native) react-native-sse or polling fallback
Rich cards React DOM components React Native components (same data, different rendering)
File upload Drag & drop + file input Camera capture + document picker
Voice input Web Speech API expo-speech or Whisper API direct
Notifications Browser push (Web Push API) APNs (iOS) + FCM (Android) via expo-notifications

Push notification setup

Registration:
  App launch → expo-notifications.getExpoPushTokenAsync()
  → POST /api/v1/devices/register { token, platform, device_id }
  → Stored in device_registrations table (existing)

Delivery:
  Backend event (booking_confirmed, driver_assigned, escalation)
  → Notification service checks patient preferences
  → Routes to push channel
  → Expo Push API → APNs/FCM → device

Offline support (P1)

expo-sqlite local database:
  ├── message_queue (unsent messages, queued when offline)
  ├── upload_queue (files selected offline, uploaded when connected)
  └── case_cache (last-fetched case data for offline viewing)

Sync strategy:
  - On reconnect: flush message_queue → API, flush upload_queue → R2
  - Conflict resolution: server wins (append-only chat model, no conflicts)
  - Cache invalidation: re-fetch case data on reconnect

Document upload flow

Patient taps camera icon
  → expo-camera opens (or expo-document-picker for existing files)
  → Image/PDF captured
  → Client-side validation (size, type — shared-core/validation)
  → POST /api/v1/uploads/presign → get presigned URL
  → Upload directly to R2 (same as web)
  → POST /api/v1/uploads/confirm → triggers OCR pipeline
  → SSE/polling for processing status
  → Rich card appears in chat with results

Development Workflow

Local development

cd packages/mobile-patient
npx expo start          # Expo dev server with hot reload
                        # Scan QR code with Expo Go app on device
                        # Or press 'i' for iOS simulator / 'a' for Android emulator

No Xcode or Android Studio needed for JS-only development. Only needed if adding native modules.

Cloud builds (EAS)

eas build --platform ios      # Cloud build for iOS (no Mac required for Android)
eas build --platform android  # Cloud build for Android
eas submit                    # Submit to App Store / Play Store

OTA updates

For JavaScript-only changes (no native module changes):

eas update --branch production    # Push JS update to all users
                                   # No App Store review needed
                                   # Users get update on next app open

This is powerful for: - Chat UI fixes - Rich card rendering updates - API client changes - Scoring display changes - Bug fixes in business logic

Native module changes (camera, notifications, biometrics) require a full App Store build.


Coordinator Mobile App

Priority: P2 — responsive web first. Native only if field coordinator use proves significant.

When to build: If coordinators at airports or hospitals need: - Quick booking confirmations on the go - Push alerts for escalations (faster than email/web) - Offline vendor contact lookup

Architecture: Same monorepo pattern — packages/mobile-coordinator/ imports shared-core/ + shared-native/.

Scope: Subset of coordinator-app — case queue, escalation alerts, quick booking actions. Not the full timeline manager or vendor directory.


Stakeholder Mobile Matrix

Stakeholder Mobile need Priority Approach
Patient High — medical travel is mobile-first P0 post-seed React Native (Expo) — this spec
Coordinator Medium — field coordinators need quick access P2 Responsive web first, native later
Provider Low — desktop dashboards P3 Responsive web only
MSO Doctor Low — clinical review is desktop work None Web only
Facilitator Low P3 Responsive web only

Estimated Effort

Prerequisites (monorepo setup — needed regardless of mobile)

Task Effort
Initialize pnpm workspace 0.5 day
Extract shared-core (types, API client) 2-3 days
Move patient-app into workspace 1 day
Extract shared-web 2-3 days
CI/CD for workspaces (Vercel per-package) 1 day
Subtotal ~7-8 days

Mobile build

Task Effort
Expo project setup + Clerk auth 2 days
shared-native scaffolding (View, Text, Pressable basics) 2 days
Chat interface (messages, rich cards, input) 3-4 days
Document upload (camera + picker + R2) 2 days
Transport panel + progress tracking 2 days
Push notifications (registration + delivery) 1-2 days
Case list + navigation 1 day
Summary view 1 day
Testing (unit + device testing) 2-3 days
EAS build setup + App Store submission 1-2 days
Subtotal ~17-22 days

Total: ~24-30 days (5-6 weeks)

Monorepo setup is a one-time cost that benefits all stakeholder apps.


Edge Cases

Scenario Handling
Patient on old iOS (< 15) or Android (< 10) Expo supports iOS 15+ and Android 10+. Show "update required" for older versions.
Push token registration fails Retry on next app open. Fall back to SMS/email for critical alerts.
Large PDF upload on poor connectivity Chunk upload with resume capability. Show progress bar. Queue if offline.
SSE not supported on mobile network Polling fallback (existing pattern). Check connectivity and switch strategy.
App killed during file upload Upload queue persists in expo-sqlite. Resume on next app open.
Patient switches between web and mobile mid-conversation Same API, same conversation state in PostgreSQL. Seamless — messages appear on both.
Biometric auth fails repeatedly Fall back to Clerk password/email auth. Never lock patient out.
Camera permission denied Show explanation dialog. Offer file picker as alternative (doesn't need camera permission).
App Store rejection (medical content) Curaway is a coordination platform, not a medical app. Include disclaimer in App Store description. No clinical advice in the app.
Expo SDK major version upgrade Pin Expo SDK version. Upgrade on a schedule (every 2-3 releases), not immediately.

Flagsmith Flags

Flag Type Default Description
mobile_patient_enabled Boolean false Feature gate for mobile app access
mobile_push_enabled Boolean true Enable/disable push notifications
mobile_offline_mode Boolean false Enable offline queueing (P1)
mobile_document_scanner Boolean false ML-enhanced document scanner (P1)
mobile_voice_input Boolean false Voice input via Whisper (P1)

App Store Strategy

iOS (App Store)

  • Category: Health & Fitness or Medical (coordination, not clinical)
  • Privacy labels: Collects name, email, health records (with consent)
  • App Tracking Transparency: Not required — no cross-app tracking (PostHog is first-party, UUID-only)
  • Review considerations: Emphasize coordination platform, not medical advice. Include disclaimer.

Android (Play Store)

  • Category: Health & Fitness
  • Health data policy: Declare health data collection, point to privacy policy
  • Data safety section: Encryption in transit + at rest, user can request deletion (GDPR cascade)

Both platforms

  • Minimum viable store listing: Screenshots of chat, transport panel, progress view
  • TestFlight / Internal Testing: SD + team test before public release
  • Staged rollout: 10% → 50% → 100% on both platforms