aimanggi
Back to projects

January 13, 2025

From PWA to Native: Designing for Complexity

Porting a complex web workspace into fully optimized iOS and Android wrapper structures utilizing performant event bridges.

ReactReact NativeTypeScript

The client’s product began as a web-first wellness platform—shared backend, and offline-friendly data fetching carried over from the PWA era (offlineFirst React Query, separate branding tokens for web and native surfaces). That worked while logging was mostly manual and hardware was optional.

As the program matured, device sync became core: Bluetooth body-composition scales, camera-based food logging with AI, push notifications that route into chat and quests, and store distribution with version gates. A responsive PWA could not own those concerns reliably across iOS and Android. We migrated to Expo SDK 53 + React Native, EAS builds, and custom native modules—without throwing away the PWA’s cache-first data habits.

The Problem

The PWA hit platform ceilings

CapabilityPWA limitation
Vendor BLE scalesWeb Bluetooth does not expose proprietary Omron, Senssun, or Qingniu SDKs; pairing and historical sync need native bridges.
Camera + AI food loggingMobile Safari and installed PWAs constrain camera permissions, performance, and background behavior vs native camera APIs.
Push → deep navigationFCM + channel control (Notifee) and cold-start routing into chat or quest screens are first-class on native, fragile on web.
App Store lifecycleMandatory update checks, OTA via Expo Updates, and per-tenant bundle IDs are store-app problems, not tab-in-browser problems.
Rich native UISkia charts, Three.js body models via native GL, and Lottie at 60fps are poor fits for a typical mobile web shell.

Complexity did not disappear—it moved

Shared environment naming (legacy web public env vars beside native Expo public vars), dual tenant builds, and three separate scale SDK integrations each with config plugins, binary frameworks, and permission flows. The migration was not “rewrite JSX”—it was vendor-native boundaries plus preserving offline assumptions from the web app.

Requirements we had to meet

  • Pair and sync smart scales (multiple brands) with reliable BLE on iOS and Android.
  • Ship on App Store and Play Store with EAS channels, runtime versions, and tenant variants.
  • Retain offline-capable quest logging inherited from the PWA data layer.
  • Defer HealthKit / Health Connect until product-ready (scaffolded in config, not shipped).

The Solution

Why native wins over PWA for this product

1. Hardware is the product differentiator
Custom Expo modules wrap vendor SDKs:

  • Omron — pairing, scan, connect, vital data transfer; iOS ships vendor xcframeworks.
  • Senssun — BLE scales with 150+ body-composition fields; cross-platform module parity documented for iOS/Android interface alignment.
  • Qingniu — Android BLE scale SDK (native bridge built; rollout phased across the app).

The Expo app config declares Bluetooth and location (Android BLE scan), injects API keys via config plugins, and documents usage strings for health peripherals. None of this is reproducible as an installable PWA with the same reliability.

2. Camera-native food quest
Food logging uses the native camera module, gallery fallback, and server-side AI analysis. Native capture paths feed offline reconcile (local file → upload on reconnect)—file handling and permissions are simpler and more predictable than mobile browser camera APIs.

3. Push as an orchestration layer
Firebase Cloud Messaging plus Notifee handles channels, badges, and press actions. Background handlers route users into in-app chat (Sendbird payloads) or scale-sync notification types. Web push cannot match Android channel semantics or iOS cold-start navigation via Expo Router.

4. Store + OTA operations
EAS Build/Submit, runtime versioning, and update channels let the client ship JS fixes without waiting for full store review where policy allows—while a store version checker enforces minimum native builds when APIs break compatibility.

5. Inherited PWA strengths, native shell
The React Query layer still prioritizes cached and offline data (networkMode: 'offlineFirst') with MMKV persistence—so migration cost focused on platform gaps, not rethinking every screen’s data pattern.

Key Outcomes

  • Store-native distribution with EAS pipelines, OTA updates, and version enforcement.
  • Bluetooth scale sync for multiple vendors via custom native modules—not a polyfill.
  • Camera + AI food pipeline with native capture and offline reconcile path.
  • Push-driven engagement with deep links into chat and notification.
  • Preserved offline-first data habits from the PWA (React Query + MMKV) inside the native shell.
  • Clear deferred scope — HealthKit/Health Connect ready in config, shipped when product demands it.

PWA vs Native — Decision Summary

DimensionPWANative app
Smart scale syncNot viable with vendor SDKsCustom BLE modules per brand
Food photo + AIConstrained camera APIsNative camera module + on-device file paths
NotificationsLimited parityFCM + Notifee + background routing
Offline quest logsService worker / cache strategiesMMKV + paused mutations + reconcile
DistributionURL / add-to-homeApp Store + Play Store + EAS OTA
3D / charts performanceHeavy WebGLSkia + native GL
AttestationOrigin-basedMobile app signature

What Changed for the Organization

PWA-firstNative companion
Browser tab, no BLE SDK accessInstalled app with paired peripherals
Email or generic web pushFCM with quest/chat routing
Responsive layout focusPermissions, background modes, store compliance
Single web deployMulti-tenant EAS builds + native binary deps
Manual weight entryScale sync notifications and automated body metrics

Lessons & Next Steps

What worked: Treating native as the hardware and notification layer while keeping the data layer familiar (React Query offline-first) reduced product risk. Splitting scale vendors into isolated modules contained SDK conflicts.

Core lesson: The hard part was not React vs React Native. It was deciding which problems only native can solve (BLE, push, store, camera files) while not rewriting what the PWA already solved well (cache-first reads and offline-capable quest writes).


Stack: Expo · React Native · EAS · Custom native modules (BLE) · FCM · Notifee · TanStack Query · MMKV