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-expoSDK - 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