Skip to content

PWA vs Native Parity Matrix

Feature parity decisions between the native Capacitor builds (iOS, Android) and the web PWA layer. This matrix exists to document rationale behind platform-specific feature gaps, preventing duplicate work and avoiding accidental feature divergence over time.

Parity Matrix

CapabilityNative (Capacitor)Web (PWA)Notes
InstallationApp Store / Play Store downloadChrome/Edge/Firefox install prompt (beforeinstallprompt) + iOS/Android Safari "Add to Home Screen"Native: automatic distribution via App Store. Web: Chrome-based browsers and Android trigger beforeinstallprompt; iOS Safari lacks the event (users must manually "Share → Add to Home Screen"). Web: less discoverability but no gatekeeping.
Push NotificationsNative APNS (iOS) / FCM (Android) + Capacitor @capacitor/push-notificationsNot implementediOS PWA push requires user to manually add to Home Screen first; iOS Safari 16.4+ only; reliability is poor in practice. Android PWA push is viable but falls below v1.15 implementation threshold given iOS friction. Rationale: Per Penny's risk analysis, iOS push-notification gap is a north-star risk for a goal-coaching app (fewer reminders = lower goal completion). Addressed by keeping native Capacitor builds as the primary mobile surface with native push reliability. Web users rely on session-based reminders. Out of scope: iOS PWA push, Web Background Sync API.
Secure Storage@aparajita/capacitor-secure-storage (native keychain on iOS, KeyStore on Android)Browser localStorage + sessionStorage (same-origin policy)Native: leverages OS-level cryptographic storage with no user-accessible plaintext. Web: localStorage is accessible to XSS attacks but gated by same-origin policy. Objectuve API tokens are short-lived (10 min, refreshed via Clerk); long-term storage is not needed. Confidence sufficient for web PWA. Risk mitigation: never store secrets longer than session TTL.
File System@capacitor/filesystem (native sandbox)No direct access; relies on blob download via browserNative: full read-write access to app sandbox; no user interaction required. Web: no access to local file system (browser sandbox). Limited to: user-initiated file download (<a download>), file input (<input type="file">). Sufficient for goal export/import flows.
Camera@capacitor/camera + @ionic/pwa-elements@ionic/pwa-elements camera polyfill (uses <input type="file" accept="image/*">)Native: direct camera access via Capacitor plugin. Web: @ionic/pwa-elements provides a shim that falls back to file picker; actual camera access via getUserMedia() is possible but requires HTTPS + CORS. PWA shim sufficient for photo uploads (goal progress evidence). Deliberate parity: both ask user to select or take a photo; only mechanism differs.
Contacts@capacitor/contacts (native contacts DB)Not implementedWeb browsers do not expose contacts API (privacy restriction). Objectuve does not require contact access for core goal-tracking. Out of scope.
Background SyncCapacitor app can sync in background; @capacitor/app-launcher and notification center enable re-engagementNot implemented (no Web Background Sync API)Web: the browser's Web Background Sync API (part of Service Workers) has near-zero iOS Safari support; Android support is partial — this app-level browser API is genuinely unused, unlike the app-level queue below. Mutation buffering for offline flows is OBJ-32's responsibility (runs in both native and web) and does auto-sync, not user-initiated-only: App.vue's watchEffect calls syncStore.processQueue() the instant useNetworkStatus() reports isOnline with a non-empty queue — see the Offline Mutation Buffering row below. User-initiated retry applies only to the separate failed list (items that exhausted MAX_ATTEMPTS), via DidntSendSheet's "Review" action (syncStore.retry(id)). Rationale: Differentiating factor for native app value prop is the browser Background Sync API specifically (background delivery while the PWA isn't foregrounded) — the app-level sync queue already gives web users automatic, no-input-required sync while the tab is open.
App Store DiscoveryListed on App Store / Play Store; searchable by app name, category, keywordsNot discoverable via app storesNative: automatic distribution via official channels; users search "goal tracker" and find Objectuve. Web: users arrive via web search, direct link, or marketing channels. This is a deliberate trade-off: web removes App Store gatekeeping friction but sacrifices organic discovery.
Install DiscoverabilityUsers encounter the app via App Store search/browseBrowser-dependent install prompt + manual "Add to Home Screen"Chrome/Edge/Firefox: beforeinstallprompt fires automatically, offering native install UI. Android Chrome: repeatable install prompt. iOS Safari: no install prompt; users must discover "Add to Home Screen" in share menu manually. Rationale: Web users are less discoverable by default but benefit from zero-friction try-before-install. Sufficient for conversion funnels that already direct to the web app (landing page, marketing links).
Network Detection@capacitor/network (platform-native network APIs)navigator.onLine + window online/offline eventsCanonical source: useNetworkStatus() composable. Both native and web must use this composable, not roll separate implementations. Ensures shared network state across both platforms for OBJ-32 mutation sync queue. Web composable reads navigator.onLine (boolean) and navigator.connection.effectiveType (connection speed); native reads Capacitor plugin. Composable handles platform branching internally.
Offline Mutation BufferingOBJ-32 Sync Queue (Pinia store + Capacitor integration)OBJ-32 Sync Queue (shared Pinia store)Parity: complete. Both native and web use identical syncStore for buffering mutations when offline. PWA service worker explicitly does NOT intercept POST /graphql — mutations go to network and fail gracefully; a non-blocking ConnectionStatusBar (chrome, not a modal — see docs/architecture/pwa-augment.md § Offline Status) reflects the offline/syncing/failed state without occluding the rest of the app; App.vue's watchEffect (via useNetworkStatus()) detects network return and auto-drains the queue — no user action required. This is deliberate: let the network layer fail loudly, let the sync queue handle retry. No mutation interception in the SW. Queue coverage is scoped to checkInHabit + addGoalEvent only; full contract (staleness, persistence boundary, failure handling) is docs/architecture/offline-contract.md. ConnectionStatusBar ships behind offline_status_bar_enabled, at 100% rollout in production as of v4.58 Phase 5b (OBJ-3800), 2026-09-17 — go-live is complete, not pending.

Deliberate Feature Gaps (Not Implemented on Web)

These are conscious decisions to keep the web PWA simpler and to reinforce native app value:

  • Push Notifications — iOS Safari PWA unreliability + complexity trade-off (see Native column)
  • Web Background Sync API — iOS Safari unsupported; not worth implementation burden for Android-only
  • Contacts API — not needed for core product
  • Biometric Auth — native @capacitor/biometric unsupported on web (Web Crypto API is lower fidelity)

These can be revisited if product priorities change or browser standards improve.

Platform-Specific Implementations

Some features have platform-specific implementations that remain intentionally divergent:

  • Installation UI: native app store vs. web beforeinstallprompt + manual share menu
  • Secure Storage: native keychain vs. browser localStorage (with TTL-based expiry guards)
  • Camera: native Capacitor plugin vs. file picker + getUserMedia() shim
  • Network Detection: native @capacitor/network vs. navigator.onLine (unified via useNetworkStatus())

Verified against source: ionic_frontend/src/App.vue (watchEffect auto-drain, lines ~442–446), ionic_frontend/src/stores/syncStore.ts (retry, discard, failed), ionic_frontend/src/components/pwa/ConnectionStatusBar.vue, ionic_frontend/src/lib/featureFlags.ts (offline_status_bar_enabled)

Last updated: 2026-09-17 (v4.58 Phase 6, OBJ-3745: corrected Background Sync row's "user-initiated retry only" claim against App.vue's auto-drain watchEffect; updated offline_status_bar_enabled rollout to 100% (Phase 5b, OBJ-3800 go-live); linked the new offline contract page)

Loading…