PWA Scope and Architecture
Decisions from the PWA scope alignment discussion (2026-05-28). Reviewed and approved by architect before spec writing.
Goal
Feature parity with native mobile apps (iOS and Android), minus hardware-dependent features. A user should be able to do anything on the PWA that they can do on the native apps, with degraded or excluded features clearly communicated.
Visual design language is a separate future effort — PWA launches with basic/functional styling.
Auth: Required (No Guest Mode)
PWA requires login/registration, same as native apps. Guest/deferred-registration is post-MVP. The existing placeholder user system (admin-created roster slots) provides a conceptual foundation for future guest support, but no current implementation supports self-service guest access.
Feature Inventory
Audited both iOS (SwiftUI) and Android (Jetpack Compose) native apps. 63 features mapped across 5 specs.
Fully Supported (16 features)
Pure UI + API — identical in browser:
Login, Register, Password Reset, Drill List + Filtering, Score Entry, Score History + Detail, Favorites, Leaderboards, Challenges, Event List + Detail, Range Bag, Scheduled Range Days, Friends, Achievements, Content Feed, Profile + Sign Out, Notification Preferences UI, Dry Fire Drill Browsing.
Supported with Offline Bridge (3 features)
Offline Score Queue (IndexedDB), Offline Status Indicator, Score Sync (manual + auto).
Degraded (7 features)
| Feature | Gap |
|---|---|
| Media Upload (photos/videos) | Browser file picker, no custom camera UI |
| Share Overlay | Canvas API composition + Web Share API (varies by browser) |
| Par Timer | Web Audio API 880Hz beep, no haptics, ~10-50ms latency |
| Add to Calendar | .ics file download instead of native intent |
| Session Security | localStorage instead of hardware-backed keystore |
Excluded for MVP (3 features)
| Feature | Reason |
|---|---|
| Push Notifications | Web Push API is viable (iOS 16.4+) but hostile permission UX and unreliable delivery vs APNs/FCM. Product decision, not technical limitation. |
| Haptic Feedback | No web API. Par timer is audio-only. |
| Notification Deep Links | No push in MVP, so no notification-triggered routing. Internal URL routing works. |
Not in native apps (no work needed)
Biometric auth, GPS/location services, background sync — none implemented in either native app.
Architecture Decisions
WASM Persistence (ScoringEngine → IndexedDB)
ScoringEngine compiles directly to WASM (no UniFFI wrapper). The UniFFI save_state() / load_state() methods are not available on the WASM path.
PWA persistence adapter:
- Save:
engine.all_local_scores()→serde_json::to_string()→ IndexedDB - Load: IndexedDB →
serde_json::from_str::<Vec<LocalScore>>()→engine.restore(scores)
apps/pwa depends on rangeday-core with default-features = false to avoid compiling UniFFI for WASM.
IndexedDB Schema
One versioned database, one object store (score_queue) keyed by user ID. Value is the serialized JSON from all_local_scores(). No broad API data caching in the first pass.
Service Worker
Caches static/shell assets only (WASM bundle, index.html, CSS, manifest). Does NOT cache or intercept ConnectRPC/API POST requests. No background sync.
Auth Token Storage
Fixed localStorage keys (rdp_access_token, rdp_refresh_token) matching the dashboard pattern. Score queue IndexedDB storage is separately keyed by user ID.
CORS
Server ALLOWED_ORIGINS must include https://my.range-day.app and https://staging-my.range-day.app. Applied via apply-secrets GitHub Action into server-env k8s secret.
Shared Components: Deferred
Share transport/auth helpers and DTO patterns if clean. Avoid pulling dashboard Dioxus components into PWA until mobile UX stabilizes.
Spec Structure: Split by Feature Area
Feature parity is the destination, not a single implementation item. Five specs:
| Spec | Scope |
|---|---|
| pwa-foundation-shell | Dioxus scaffold, auth, router/shell, Tailwind, manifest, service worker, build/hosting, CORS |
| pwa-scoring-offline | Drill list, score entry, ScoringEngine, IndexedDB queue, sync, score history/detail, void |
| pwa-events-rangebag | Events, range bag, scheduled range days (free-text location), calendar export |
| pwa-social-training | Friends, challenges, leaderboards, achievements, content feed, favorites |
| pwa-media-dryfire | Media upload/gallery/share overlay, dry fire browsing, par timer |
Implementation order: foundation first, scoring-offline second (hardest lift), remaining three parallelizable.
Data Shape Notes
- Content feed unread dots: Client-side derived (comparing entry IDs against locally-tracked read set), not server-provided. Same approach as native apps.
- Achievement icons/colors: Deterministic local mapping from achievement name/type — not API-provided fields.
- Scheduled range day location: Free-text
location_textfield (max 200 chars), matching native app model. Location-API picker is post-MVP. - Content feed
action_url: Exists in proto but may be unset in server v1. Render conditionally.
Native App Screen Hierarchy (reference)
Auth Gate
├── Login
├── Register
└── Password Reset
Main Shell (Tab-based)
├── Training Tab
│ ├── Drill List (live fire / dry fire filter)
│ │ ├── Score Entry
│ │ ├── Score Queue (pending / syncing / synced / failed)
│ │ ├── Score History
│ │ │ └── Score Detail (zones, media gallery, share overlay)
│ │ └── Leaderboard (all users / friends)
│ ├── Favorites
│ ├── Dry Fire
│ │ └── Par Timer
│ └── Challenges
│ ├── Challenge Detail
│ └── Create Challenge
├── Events Tab
│ ├── Event List (org events + personal range days)
│ └── Event Detail (drills, courses, roster)
├── Range Bag Tab
│ ├── Gear List (add / edit / delete)
│ └── Scheduled Range Days (add / edit / delete / calendar export)
└── Profile Tab
├── Friends (list / requests / search)
├── Achievements (earned / locked)
├── Content Feed (messages / announcements)
├── Notification Preferences
└── Sign Out
What’s NOT in Scope
- Visual design language / branding (separate cross-platform effort)
- Guest/deferred registration (post-MVP)
- Push notifications (post-MVP, product decision)
- Background sync (not supported, foreground only)
- Broad API data caching in IndexedDB (deferred)
- Location entity picker for scheduled range days (post-MVP, free-text for now)