Skip to content

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 from chipi.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 /posters adapt 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

IDRequirement
M1Installs from Google Play on Android 10+ and the App Store on iOS 16+
M2All 14 routes (11 primary + 3 read-only) render correctly at 360 px–412 px viewport widths (common Android range)
M3All 14 routes render correctly at 375 px–430 px viewport widths (iPhone SE through iPhone 15 Pro Max)
M4Three.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
M5Core 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
M6The Lambert worker (per ADR-022) runs correctly in the WebView context on both platforms
M7Touch interactions work: orbit (one-finger drag), zoom (pinch), tap (object selection), per-route hit-testing via window.__pickAt (ADR-056)
M8Bottom-sheet panel pattern (already shipped per ADR-018) is verified in the Capacitor WebView
M9App icons + splash screens meet Google Play + App Store submission requirements
M10Privacy policy page exists and is linked from each store listing
M11Bundle-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
M12The 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

IDRequirement
S1Native share-sheet integration via @capacitor/share — "Share this mission arc" surfaces a native Android / iOS sheet
S2Haptic feedback on mission events (TLI confirmed, Mars arrival) via @capacitor/haptics
S3Status bar / system bar styling matches --color-bg (#04040c) on both platforms — no white flash between launch and first paint
S4Safe-area insets respected — Android gesture nav bar + status bar; iOS notch + Dynamic Island + home indicator
S5App state preserved across background/foreground cycle — no full reload, WebGL context restored if lost (more critical on iOS WKWebView than Android Chromium)
S6Deep 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:

ShippedWhereNotes
Hamburger nav drawer ≤ 500 pxcommit 5f4cc05aAll 11 primary routes accessible; locale picker + lens + contrast toggles next to it
Horizontal layer-filter chip flowcommit 5f4cc05aFirst chip stays anchored; remaining chips wrap rightward instead of stacking vertically
/fly HUD collapsible panelscommit 4addaca9e + 0020f128bFive panel toggles in a top-right strip — HUD, CAPCOM, Flight Director, Layers, Conics
/science compact railtask #117Right rail shrinks from 70 % viewport to a tight nav strip
Footer / CTA clearancetask #115Persistent bottom-right footer doesn't overlap landing-page or /plan action buttons
Per-planet pill colorstask #112Mars red / Earth blue / Moon grey / Explore teal — visual distinction without relying on placement
Bottom-sheet panel patternADR-018Right drawer on desktop; bottom sheet ≤ 768 px, swipe-down to dismiss
Touch readiness signalsADR-056window.__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

CriterionMeasure
Ships on both storesGoogle Play listing live + App Store listing live
Offline verifiedAmended 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 verified60 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 interactionsOrbit, 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 ratingNo forced rejections due to wrapper-policy violations
Bundle sizeAmended 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 continuityAmended 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

DependencyStatusNotes
RFC-017 / PRD-014 (Surface Hotspots) shippedRequired firstMobile 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 pageRequired before either store submissionStatic HTML page in static/privacy.html (or a SvelteKit /privacy route) — content is short because no data is collected
Google Play developer accountRequired$25 one-time
Apple Developer accountRequired$99 / yr
App icon designRequiredSee open question Q4
Bundle-size reduction strategyRequired before either store submissionRFC-018 § bundle-strategy proposes the cuts
Mobile-viewport UXS updateOptionalThe 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.

  1. Bundle-size strategy — RESOLVED SUPERSEDED (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 from chipi.github.io (RFC-018 §8.1), bundle code + textures + core JSON + default locale on-device, ~150–250 MB target. Lazy locales (§4.2) retained.

  2. Data-update mechanism — RESOLVED SUPERSEDED (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.

  3. 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.

  4. 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

  1. 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.

  2. 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.

  3. 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

Orrery — architecture documentation · MIT · No tracking