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
| Capability | Native (Capacitor) | Web (PWA) | Notes |
|---|---|---|---|
| Installation | App Store / Play Store download | Chrome/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 Notifications | Native APNS (iOS) / FCM (Android) + Capacitor @capacitor/push-notifications | Not implemented | iOS 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 browser | Native: 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 implemented | Web browsers do not expose contacts API (privacy restriction). Objectuve does not require contact access for core goal-tracking. Out of scope. |
| Background Sync | Capacitor app can sync in background; @capacitor/app-launcher and notification center enable re-engagement | Not 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 Discovery | Listed on App Store / Play Store; searchable by app name, category, keywords | Not discoverable via app stores | Native: 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 Discoverability | Users encounter the app via App Store search/browse | Browser-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 events | Canonical 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 Buffering | OBJ-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/biometricunsupported on web (Web CryptoAPI 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/networkvs.navigator.onLine(unified viauseNetworkStatus())
Related Issues & References
- OBJ-226 — PWA Augment (parent epic)
- OBJ-32 — Offline-First: Optimistic UI & Sync Queue (consumes
useNetworkStatus) - OBJ-226 — PWA Augment strategy and risk analysis
docs/architecture/pwa-augment.md— detailed architecture docsrc/composables/useNetworkStatus.ts— canonical network-detection composable
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)