ADR-078 — Mobile wrapper reshaped: iOS-first + stream-heavy bundle (reverses RFC-018 Android-first + §8.2)
Status · Accepted Date · 2026-07-06 · Addendum 2026-07-07 (Capacitor 8 + iOS safe-area shim) Gates · v0.8 Capacitor mobile wrapper (epic #149); supersedes two locked decisions in PRD-015 / RFC-018 (2026-05-16)
Addendum 2026-07-07. Two implementation decisions from the first on-simulator run:
- Capacitor 7 → 8. RFC-018 §2's "Capacitor 7" was version-at-writing (May); upgraded to Capacitor 8.4.1 via
cap migratewhile the scaffold was still fresh (deployment target → iOS 15, Android needs JDK 21). No reason to pin 7.- iOS safe-area via a native shim, not CSS
env(). Capacitor's iOS WKWebView returnsenv(safe-area-inset-*)= 0 (measured), so the nav/footer render under the status bar / Dynamic Island. ACAPBridgeViewControllersubclass (ios/App/App/AppDelegate.swift→SafeAreaViewController, wired viaMain.storyboard) reads the nativeview.safeAreaInsetsand injects them as CSS vars (--safe-area-inset-*); CSS readsvar(--safe-area-inset-top, env(...)). The@capacitor-community/safe-areaplugin was added (it's mainly an Android edge-to-edge tool) but does not fix iOSenv(). Locale flags dropped (flag emoji are unreliable cross-platform — iOS Simulator can't render them; theEN/ES/FRshortTag remains).
Gating sentence: the v0.8 Capacitor mobile wrapper ships iOS-first (App Store before Google Play, reversing the RFC-018 Android-first lock) and slims the on-device bundle by streaming all gallery imagery and narration audio from chipi.github.io (RFC-018 §8.1 network-aware service worker, selected over the deferred §8.2 disable-SW plan) because the real 2026-07 build is ~2 GB — not the 355 MB RFC-018 modelled — and non-executable JSON/image/audio streaming is App Store §2.5.2-clean; the trade is that PRD-015 M5 "fully offline" relaxes to "core experience offline from install, galleries + audio offline after first view".
Context
RFC-018 and PRD-015 froze the mobile plan on 2026-05-16 as Draft v0.3. Seven weeks of main (image pipeline v2, /fly cinematic + tour narration, 14-locale expansion) invalidated the two load-bearing assumptions the plan rested on. A re-assessment on 2026-07-06 measured current main:
1. The bundle is ~6× larger than modelled, and the mix changed
RFC-018 §4 modelled a 355 MB naive build and proposed three slimming tactics to reach ~85 MB. Measured today (static/ copies wholesale into build/ via adapter-static, svelte.config.js):
| Bucket | RFC-018 (May) | Measured (2026-07-06) |
|---|---|---|
static/images/ (galleries, heroes, surfaces) | ~120 MB | 1.6 GB |
static/audio/ (/fly narration, Curator Tour) | not modelled | 97 MB |
static/data/ (mission/site/fleet JSON, overlays) | ~150 MB @ 12 locales | 67 MB @ 14 locales |
static/textures/ (planet surfaces) | ~30 MB, "8K" | 64 MB, max 4K (tiered 2k/4k LOD) |
static/ total | ~355 MB | ~1.9 GB |
A naive cap sync therefore ships a ~2 GB app — 10× the iOS 200 MB OTA install cap and a guaranteed poor-experience rejection long before the 4 GB binary limit.
2. Two of RFC-018's three slim tactics are moot or already done
- §4.1 (swap 8K→4K textures, "saves 22 MB") — there are no 8K textures on disk; max is 4K, and a 2k-default / 4k-on-approach LOD already shipped (
src/lib/three/fly-cislunar-scene.tsupdateTextureLod()). Tactic is dead. - §4.3 (256-px fleet thumbnails, hero streaming) — never built.
pickHero()(src/lib/image-hero.ts:125) resolves 100% local paths. This tactic is the only one still worth landing, and the growth ofstatic/images/to 1.6 GB makes it central rather than incremental. - §4.2 (lazy locales) — still valid; 14 locales now, not 12.
3. Marko reverses the Android-first lock
PRD-015 §Goal and RFC-018 §9 locked Android-first ("Google Play ships before App Store", 2026-05-16) on the reasoning that Android Internal Testing stands up faster and cheaper than TestFlight. Marko's 2026-07-06 direction is iOS-first, Android next. The trade-off is understood and accepted: iOS front-loads the two hardest gates —
- the 200 MB OTA cap (stricter than Play's ~150 MB soft flag), forcing the bundle fix to be complete before the first TestFlight rather than deferrable, and
- WKWebView aggressive WebGL context-loss on background (RFC-018 §11.2, iOS-only correctness gate).
Front-loading the hard 20% is the point: "MVP" becomes "the hard problems solved first", with no false sense of progress from cheap plugin wins.
Decision
D1 — iOS-first sequencing
The v0.8 wrapper targets iOS / App Store first, Android / Google Play second. This reverses PRD-015 §Goal ("Android and iOS in parallel" / "Android Internal Testing comes online faster… iteration loop runs Android-first") and RFC-018 §9–§10 ("first to ship"). The Capacitor core, capacitor.config.ts, plugin set, and web-side slimming are platform-neutral and unaffected; only the build/submit/test ordering flips. Child issues #200/#201 (iOS) move ahead of #198/#199 (Android) in the epic.
D2 — Stream-heavy bundle (select RFC-018 §8.1 over §8.2)
Adopt the network-aware service-worker strategy that RFC-018 §8 recommended against for v1.0. The on-device bundle carries only:
- app shell + Three.js + Svelte chunks (code),
- planet textures (2k default, 4k LOD),
- core mission/site/fleet JSON (the data model that drives the 3D scenes),
- the default locale bundle (others lazy per §4.2).
Streamed from chipi.github.io, SW-cached on first view:
- all
static/images/galleries + non-critical heroes (the 1.6 GB), - all
static/audio/narration + Curator Tour (the 97 MB).
This is App Store §2.5.2-clean: nothing streamed is executable — images, audio, and JSON are inert content. The frozen-at-install code path (Three.js, Svelte) never refreshes at runtime. RFC-018 §8.2's original reason for deferring §8.1 (App Store review caution on the first ship) is outweighed now that §8.1 is the only path under the OTA cap.
D3 — M5 offline requirement relaxed
PRD-015 M5 ("App functions fully offline… all 14 routes with no network") is relaxed to:
Core experience offline from install (all 3D scenes, physics, navigation, science text, core data render with no network). Galleries + narration audio are offline after first view (SW-cached on tap while online).
The airplane-mode success criterion (PRD-015 §Success) is amended to test the core experience, with a documented note that gallery imagery + audio require one online view to cache. This is the explicit cost of D2 and is accepted.
D4 — Docs cascade
PRD-015 → v0.4, RFC-018 → v0.4, both citing this ADR. RFC-018 §4 is rewritten with measured numbers and the streaming architecture; §8 resolution flips to §8.1; §9–§11 re-sequence iOS-first. A tracking issue under epic #149 records the re-plan.
Consequences
Positive
- The bundle target becomes achievable (est. ~150–250 MB on-device vs an impossible ~2 GB), unblocking any iOS submission.
- iOS-first surfaces the WebGL context-loss and OTA-cap risks while they are cheapest to fix.
- The streaming manifest (
build-info.json, RFC-018 §8.1) becomes the future content-update channel for free — reopening the deferred RFC-018 O.Q. 5 as in-scope.
Negative / risks
- Offline regression vs the shipped PWA: first gallery/audio view now requires network. Mitigated by SW-caching after first view and by keeping the core experience fully offline. Must be called out in the store listing ("some media loads on first view").
- App Store review still novel: §8.1 has not been reviewed by Apple for this app. If Apple objects to the runtime cache despite §2.5.2 compliance, fallback is a route-subset local bundle (the re-assessment's rejected "local-slim" option). Rollback path exists.
- Streaming infra dependency: the app now hard-depends on
chipi.github.ioavailability for media. Acceptable — it is the same origin the browser build already serves from, on GitHub's SLA.
Rollback: D2 is a build-time asset-partition + SW-config change; reverting to a local bundle is a config flip (at the cost of exceeding the OTA cap). D1 is pure ordering. No data migration, no irreversible step.
Status of superseded decisions
- PRD-015 Resolved-decision framing "Android Google Play ships first regardless" (§iOS-signing, Resolved #4) — superseded by D1 for sequencing; the signing-ownership fact stands.
- RFC-018 §15 Resolved #1 ("8.2 disabled SW under Capacitor for v1.0") — superseded by D2 (now §8.1).
- RFC-018 §15 Resolved #2 (~85 MB via §4 three-tactic plan) — superseded by D2; new target ~150–250 MB via streaming, since §4.1 is moot and the image bucket grew 13×.
References
- Epic #149 · PRD-015 · RFC-018
- ADR-029 (PWA service worker), ADR-057 (locale cookie), ADR-058 (cislunar camera), ADR-046 (texture downsample pipeline)
- Re-assessment session: 2026-07-06 (branch
mobile)
Orrery · ADR-078 · Mobile wrapper reshaped — iOS-first + stream-heavy bundle · July 2026