PRD-015 — Mobile App · Capacitor wrapper
Status · Draft v0.4 (iOS-first + stream-heavy bundle per ADR-078, 2026-07-06) Date · 2026-05-16 · Amended 2026-07-06 (ADR-078) Owner · Marko
v0.4 amendment (ADR-078, 2026-07-06). Two 2026-05-16 locks are reversed after a re-assessment against current
main: (1) sequencing flips to iOS-first (App Store before Google Play); (2) the bundle strategy flips to stream-heavy — gallery imagery + narration audio stream fromchipi.github.io(SW-cached) rather than bundling, because the real build is ~2 GB, not the 355 MB modelled in May. M5 "fully offline" is correspondingly relaxed (see M5 + Success criteria below). ADR-078 is the governing record; the original May text is retained for the audit trail with inline amendment notes. Audiences · curious learner, STEM student, educator, space enthusiast Closes into · RFC-018 (Capacitor mobile integration) Slice gate · v0.8 (post-RFC-017 surface hotspots in v0.7) Why this is a PRD · Orrery already runs in mobile browsers, but a browser tab is the wrong container for the product. PRD-015 commits to distributing Orrery as a native Android + iOS app via Capacitor, with the existing SvelteKit web app as the source of truth and the native shell adding only what Google Play and the App Store require: an icon, a splash, a privacy policy, a system share sheet, and the ability to install from a store rather than from a URL bar. The user-value argument is reach (the audience reaches for their phone, not their laptop) and platform legitimacy (an installed app is treated differently from a bookmarked tab). The PRD locks scope to "wrap and ship the same product"; no native screens, no rewrite, no parallel codebase.
Problem
Orrery is a browser-first application that runs well in a mobile browser — Three.js renders in mobile Safari and Chrome WebView, the v0.6 mobile pass shipped a hamburger nav, horizontal-flow layer chips on /explore /earth /moon /mars, a collapsible /fly HUD, a compact /science rail, and footer/CTA clearance — but a browser tab is still the wrong container.
The 3D solar system, the mission arc, the porkchop plot, the cislunar Earth-centred view, the ISS module-pickability, the Mars surface map: these are experiences, not pages. They belong on a home screen and in an app drawer.
Mobile browser distribution also limits reach. App-store presence is the discovery channel for Orrery's audience — students, curious people, educators. The curious person who watched a Mars landing reaches for their phone, not a laptop, and they reach for the app store, not a search engine.
There is also a capability gap. PWA install on iOS is hidden behind Safari's share sheet; Android's "Add to home screen" is more discoverable but still requires a browser visit first. A native wrapper is also the prerequisite for mobile-first features — system share sheet for mission arcs, haptics on trajectory events, native deep links — that a browser tab can't surface.
Goal
Ship Orrery as a native application on iOS first, Android next, distributed via the App Store and Google Play, using Capacitor as the wrapper layer.
The web application is the product. Capacitor provides only the shell.
Sequencing — iOS-first (amended ADR-078, 2026-07-06). iOS / App Store is the first ship target; Android / Google Play follows. This reverses the original May plan (Android-first, "co-equal parallel"). Rationale: iOS front-loads the two hardest gates — the 200 MB OTA install cap (stricter than Play's ~150 MB soft flag, so the bundle fix must be complete before the first TestFlight) and WKWebView's aggressive WebGL context-loss on background (an iOS-only correctness gate, M5/S5). Solving the hard 20% first removes the false-progress risk of cheap plugin wins. The Capacitor core, config, and web-side slimming are platform-neutral; only build/submit/test ordering flips.
Original May text (superseded): "Ship on Android and iOS in parallel… Android Internal Testing comes online faster than TestFlight ($25 one-time vs $99/year), so the practical iteration loop runs Android-first." The cost trade-off (Android cheaper/faster to iterate) is real and accepted as the price of iOS-first.
Non-goals
- Not a rewrite. The SvelteKit application is the source of truth. Capacitor wraps the static
build/output — it does not replace any of it. - Not new screens. No mobile-only routes; the existing 11 primary nav routes (
/explore·/plan·/fly·/missions·/earth·/moon·/mars·/iss·/tiangong·/science·/fleet) plus/credits/library/postersadapt to mobile viewports as already shipped in v0.6. - Not native UI components. No UIKit, no Jetpack Compose, no native nav. The Capacitor WebView renders the SvelteKit app as-is.
- Not background processing. No background fetch, no push notifications, no background location. Launch-window countdowns are a Phase 3 consideration.
- Not enterprise distribution. Public Google Play + public App Store only. MDM / enterprise profiles out of scope.
- Not a parallel data layer. Mission JSON ships bundled with the app. Updates ride on app-version submissions plus the existing PWA service worker (autoUpdate per the May 2026 PWA refresh).
Requirements
Must have
| ID | Requirement |
|---|---|
| M1 | Installs from Google Play on Android 10+ and the App Store on iOS 16+ |
| M2 | All 14 routes (11 primary + 3 read-only) render correctly at 360 px–412 px viewport widths (common Android range) |
| M3 | All 14 routes render correctly at 375 px–430 px viewport widths (iPhone SE through iPhone 15 Pro Max) |
| M4 | Three.js r128 (per ADR-001) renders in Chromium WebView (Android 10+) and WKWebView (iOS 16+) for all 7 3D scenes: /explore, /fly heliocentric, /fly cislunar (per ADR-058), /earth, /moon, /mars, /iss, /tiangong |
| M5 | Core experience functions offline from install — all 7 3D scenes, physics, navigation, science text, and core mission/site/fleet data render with no network. Gallery imagery + narration audio are offline after first view (streamed from chipi.github.io and SW-cached on tap while online). Amended ADR-078 (2026-07-06): relaxed from "fully offline" as the accepted cost of the stream-heavy bundle (D2/D3). Original: "App functions fully offline via the v0.6 PWA service worker (ADR-029)." Must be verified in the Capacitor container on both platforms |
| M6 | The Lambert worker (per ADR-022) runs correctly in the WebView context on both platforms |
| M7 | Touch interactions work: orbit (one-finger drag), zoom (pinch), tap (object selection), per-route hit-testing via window.__pickAt (ADR-056) |
| M8 | Bottom-sheet panel pattern (already shipped per ADR-018) is verified in the Capacitor WebView |
| M9 | App icons + splash screens meet Google Play + App Store submission requirements |
| M10 | Privacy policy page exists and is linked from each store listing |
| M11 | Bundle-size strategy is documented + measured. Amended ADR-078 (2026-07-06): the naive build is ~2 GB as of 2026-07 (static/images/ 1.6 GB + static/audio/ 97 MB + data + textures), not the 355 MB modelled in May. The stream-heavy strategy (ADR-078 D2 / RFC-018 §8.1) bundles code + textures + core JSON + default locale on-device (~150–250 MB target) and streams galleries + audio from chipi.github.io (SW-cached). Required before either store submission; must clear the iOS 200 MB OTA cap before the first TestFlight |
| M12 | The single orrery_locale cookie permitted by ADR-057 works in the Capacitor WebView on both platforms (or a Capacitor Preferences shim is added) |
Should have
| ID | Requirement |
|---|---|
| S1 | Native share-sheet integration via @capacitor/share — "Share this mission arc" surfaces a native Android / iOS sheet |
| S2 | Haptic feedback on mission events (TLI confirmed, Mars arrival) via @capacitor/haptics |
| S3 | Status bar / system bar styling matches --color-bg (#04040c) on both platforms — no white flash between launch and first paint |
| S4 | Safe-area insets respected — Android gesture nav bar + status bar; iOS notch + Dynamic Island + home indicator |
| S5 | App state preserved across background/foreground cycle — no full reload, WebGL context restored if lost (more critical on iOS WKWebView than Android Chromium) |
| S6 | Deep links work — orrery://fly?mission=curiosity opens the mission-arc screen directly. Android App Links + iOS Universal Links (https://chipi.github.io/orrery/fly?mission=curiosity) deferred to Phase 3 |
Will not have (this phase)
- Push notifications for launch-window alerts
- Background fetch for mission data updates (PWA autoUpdate covers refresh-on-next-launch)
- Cloud sync for saved missions (Google Drive / iCloud)
- Wear OS / Apple Watch companion
- Widgets (Android / iOS)
- In-app purchases of any kind
- AR / spatial-anchored views (separate ADR if pursued)
What's already shipped (mobile-readiness inventory)
Tasks delivered in the v0.6 mobile-pass that PRD-015 inherits:
| Shipped | Where | Notes |
|---|---|---|
| Hamburger nav drawer ≤ 500 px | commit 5f4cc05a | All 11 primary routes accessible; locale picker + lens + contrast toggles next to it |
| Horizontal layer-filter chip flow | commit 5f4cc05a | First chip stays anchored; remaining chips wrap rightward instead of stacking vertically |
/fly HUD collapsible panels | commit 4addaca9e + 0020f128b | Five panel toggles in a top-right strip — HUD, CAPCOM, Flight Director, Layers, Conics |
/science compact rail | task #117 | Right rail shrinks from 70 % viewport to a tight nav strip |
| Footer / CTA clearance | task #115 | Persistent bottom-right footer doesn't overlap landing-page or /plan action buttons |
| Per-planet pill colors | task #112 | Mars red / Earth blue / Moon grey / Explore teal — visual distinction without relying on placement |
| Bottom-sheet panel pattern | ADR-018 | Right drawer on desktop; bottom sheet ≤ 768 px, swipe-down to dismiss |
| Touch readiness signals | ADR-056 | window.__pickAt(x, y) test hook + data-route-ready + data-loading attributes on every canvas route |
The mobile viewport adaptation work that earlier drafts of this PRD pre-empted is already done. PRD-015's primary work is the wrap + distribute layer.
Distribution
Sequencing note (ADR-078): the subsections below retain their original "first to ship" / ordering language from the May draft. Per ADR-078 the ship order is now iOS App Store first, Google Play second — read "first to ship" as historical. The platform-specific requirements in each subsection are unchanged.
Android — Google Play (now second to ship)
Less strict than Apple for educational content; faster to iterate on.
- Target API level 35 (Android 15) as of the August 2025 Play requirement; revisit before submission.
- Privacy policy required (links to a page hosted on
chipi.github.io). - Listing category: Education.
- Internal Testing track is available without a public listing — useful for the first physical-device runs before promoting to Production.
- Closed Testing for an external test cohort (≥ 12 testers, 14-day opt-in) when needed before Production.
Expected review cycle: 1 submission, automated review passes within hours for educational content.
iOS — App Store
Apple scrutinises web-wrapper apps. Acceptance criteria for a web-app-as-native:
- The app must provide clear educational value beyond pointing at a URL. Orrery qualifies: 11 primary routes, 85-section science encyclopedia, 137-entry fleet, 71 hand-authored SVG diagrams, real Keplerian + Lambert physics, fully offline-capable.
- Must not be a thin wrapper around a website that could be accessed in Safari. Orrery's offline capability + native integrations (Share, Haptics) + bundled-data model distinguish it from a Safari bookmark.
- Privacy policy required. Orrery collects no personal data per ADR-057 + the project's privacy stance; the policy states this explicitly.
- Listing category: Education primary, Reference secondary.
Expected review cycle: 1–2 submissions before approval. Apple commonly requests screenshots proving offline functionality.
Store metadata (both)
- App name: Orrery — Solar System Explorer
- Subtitle (iOS) / Short description (Play): Mission simulator + orbital mechanics
- Long description: adapted from the docs landing (
docs/index.md) - Screenshots: 8–10 per device class (Android phone, Android tablet, iPhone, iPad) — pull from
docs/screenshots/(already curated for the README + user guide) - Privacy policy URL: hosted at
https://chipi.github.io/orrery/privacy - Keywords (iOS): solar system, orbital mechanics, Mars, mission simulator, space, NASA, astronomy, education, Apollo, ISS
Success criteria
| Criterion | Measure |
|---|---|
| Ships on both stores | Google Play listing live + App Store listing live |
| Offline verified | Amended ADR-078: core experience of all 14 routes functions with no network on a physical device (airplane-mode test) — 3D scenes, physics, navigation, science text, core data. Gallery imagery + narration audio require one online view to SW-cache; documented as expected behaviour, not a defect |
| Three.js verified | 60 fps orbit rendering on a mid-range Android (Snapdragon 778G or equivalent) and iPhone 12 (A14); 30 fps acceptable on /iss + /tiangong (heaviest scenes) |
| Lambert worker | /plan porkchop renders within 5 s on the target devices for a single destination |
| Touch interactions | Orbit, zoom, object selection, panel-tab switching all work correctly on touch screens on both platforms |
| Cislunar view | /fly?mission=apollo11 switches to the cislunar Earth-centred camera correctly per ADR-058 |
| App store rating | No forced rejections due to wrapper-policy violations |
| Bundle size | Amended ADR-078: on-device install size clears the iOS 200 MB OTA cap after the stream-heavy work (RFC-018 §8.1); target ~150–250 MB. The May "≤ 80 MB" figure assumed the obsolete 355 MB baseline and is void |
| PWA continuity | Amended ADR-078: the service worker registers under Capacitor and its runtimeCaching streams + caches gallery imagery + audio from chipi.github.io (RFC-018 §8.1). Frozen-at-install code (Three.js, Svelte chunks) never refreshes at runtime — §2.5.2-clean. The browser build's autoUpdate behaviour is unchanged |
Dependencies + sequencing
| Dependency | Status | Notes |
|---|---|---|
| RFC-017 / PRD-014 (Surface Hotspots) shipped | Required first | Mobile distribution should ship the best-case desktop experience of landing-site exploration first, not a degraded mid-pass version. v0.7 lands the Mars/Moon hi-res imagery + per-mission 3D models; v0.8 wraps for mobile |
| Privacy policy page | Required before either store submission | Static HTML page in static/privacy.html (or a SvelteKit /privacy route) — content is short because no data is collected |
| Google Play developer account | Required | $25 one-time |
| Apple Developer account | Required | $99 / yr |
| App icon design | Required | See open question Q4 |
| Bundle-size reduction strategy | Required before either store submission | RFC-018 § bundle-strategy proposes the cuts |
| Mobile-viewport UXS update | Optional | The v0.6 mobile work shipped without a dedicated UXS; documenting the patterns retroactively (similar to how ADR-052/053/054 documented Fleet retrospectively) is a clean-up task |
Resolved decisions
All four blocking questions resolved 2026-05-16. Tracked here for the audit trail; new questions go below. Items 1 & 2 superseded by ADR-078 (2026-07-06) — see amendment notes.
Bundle-size strategy —
RESOLVEDSUPERSEDED (ADR-078). Original: ship RFC-018 §4 slim plan (4K textures, lazy locales, 256-px thumbnails), ~85 MB target. Superseded because the May baseline (355 MB) was measured at ~2 GB in July; §4.1 (8K→4K) is moot (no 8K exists) and the image bucket grew 13×. New resolution (ADR-078 D2): stream galleries + audio fromchipi.github.io(RFC-018 §8.1), bundle code + textures + core JSON + default locale on-device, ~150–250 MB target. Lazy locales (§4.2) retained.Data-update mechanism —
RESOLVEDSUPERSEDED (ADR-078). Original: PWA SW disabled under Capacitor for v1.0 (§8.2). Superseded because §8.1 (network-aware SW) is now the only path under the OTA cap and is §2.5.2-clean (streamed content is inert JSON/image/audio). New resolution (ADR-078 D2): ship §8.1. The content-update channel (build-info.json) comes free as a consequence.Tablet — RESOLVED. Ship as-is for v1.0. Desktop layout fits 1024+ reasonably (
/explore,/missions,/science,/fly); no new breakpoint code. Mark tablets as "supported, not optimised" in the Play / App Store listings. Revisit after first physical-device testing.iOS code-signing — RESOLVED. Marko owns the Apple Developer membership (personal account, $99/yr). Listed as the developer on the App Store. Resolution applies whenever iOS submission begins (Android Google Play ships first regardless).
Open questions
App icon design. The Bebas Neue wordmark doesn't translate to a 60×60 px home-screen icon. Options:
- Stylised orrery-as-clockwork-model (concentric rings + a planet)
- Lambert porkchop shape (the V-shape that's the project's most distinctive visual)
- Simple star + ring on
#04040c
Open design task; not blocking until after v0.7 work lands.
Internal Testing cohorts. Need at least 5 Android testers (range: Pixel through Galaxy mid-range) and ~5 iOS testers (range: SE through Pro Max) with real devices. Recruitment plan TBD — likely "ship to Internal Testing track + post in chipi org channels" rather than active recruiting.
Per-scene WebGL context-loss restore (7 scenes). RFC-018 §11.2 requires a small
reinit()factor-out on/explore,/fly(heliocentric),/fly(cislunar per ADR-058),/earth,/moon,/mars,/iss,/tiangong. Scope as 7 sub-tasks under M5 when implementation begins; not an architectural decision.
Orrery · PRD-015 · Mobile App — Capacitor Wrapper · May 2026 (amended July 2026, ADR-078)See also: ADR-078 iOS-first + stream-heavy bundle · RFC-018 · PRD-014 Surface Hotspots · ADR-018 Mobile-first design · ADR-029 Service worker / PWA · ADR-057 Locale cookie carve-out · ADR-058 Cislunar second camera