SHOT ships as a Capacitor 6.1.2 native shell wrapping a Vite/React web bundle. Every UI tweak, copy fix, or non-native bug fix today goes through a full App Store / Play Store review cycle — 24-48h on Apple, 1-3h on Google. We want to ship JS/HTML/CSS-only changes to live users without that round-trip, while keeping native changes (new Capacitor plugins, entitlement/permission changes, paywall logic) on the store-review track.
Three pains drove the question: (1) hot-fix dead-time when a Pulse layout bug or copy mistake ships, (2) no granular rollback below a full native re-release, and (3) reviewer-cycle anxiety on minor UI changes that are visually obvious to test but tediously slow to ship.
Forces in tension
Ship UI/copy/layout fixes without 24-48h Apple review
BYO signing keys — SHOT-owned, EU-resident, no third-party trust
Apple DPLA §3.3.1(B) — must not change primary purpose or features post-review
Stripe / RevenueCat / paywall flows cannot OTA (§3.1.1)
COPPA + DSA Art.14 visibility on minor consent + UGC moderation
No per-seat vendor pricing (Stevie's Vercel allergy)
02
Options considered
Three delivery models, all built around the same MPL-2.0 plugin (@capgo/capacitor-updater@lts-v6). The vendor landscape collapsed in 2025 — Microsoft App Center / CodePush shut down March, Ionic Appflow closed to new signups in February.
Same MPL-2.0 plugin, but pointed at SHOT-owned R2 + edge function. Ed25519-signed manifests, BYO keys.
~$0.05/yr
EU R2 (WEUR)
BYO Ed25519
SHOT-owned data flow
Reuses existing infra
8-10 day build
Two storage shapes to operate
Effortmed
Fitstrong
Risklow
Option CEOL
Ionic Live Updates / Appflow
Historical incumbent. New signups closed Feb 2025. Full shutdown 2027-12-31. Not a real option for new builds.
Tightest Capacitor fit (was)
Closed to new customers
Per-seat pricing ($500+/mo)
2-year shutdown clock
Plugin unmaintained
Effortn/a
Fitn/a
Riskfatal
03
Decision
B
We will adopt
Self-host OTA on Cloudflare R2 + Supabase edge function
Option B reuses every piece of infrastructure SHOT already runs (R2 announcements buckets, Supabase edge-fn pattern, transcrypt-encrypted env, GitHub Actions workflow_dispatch), gives us EU-resident bundle delivery (R2 WEUR), and keeps signing keys under SHOT control. Capgo Cloud (Option A) is a perfectly good fallback if dev time becomes the binding constraint — migration cost is low (plain zip + open plugin) and the architecture preserves the option to flip back with a single config field. Appflow (Option C) is eliminated by EOL — not a decision so much as a fact.
04
Resulting architecture
Publish path (GitHub Actions → R2 + Postgres) sits on top. Runtime check path (App → edge fn → R2 presigned URL) sits at the bottom. Bundle apply happens on next cold-start after background download.
Server-flip rollback recovers fleet within hours on bad bundle
Capabilities-manifest catches plugin-version skew before crash
Negative
~8-10 days to production-grade vs ~1 day for Capgo Cloud
Two storage shapes operationally (R2 + Postgres ota_*)
Ed25519 public-key rotation requires a new native release
13 must-not-OTA surfaces become discipline AND CI-enforced
Native/JS version skew risk if capabilities-manifest drifts
Neutral
Web (Vercel) bundle continues to roll separately — only native shell uses OTA
App Store / Play release pipeline unchanged — native still ships via build-release.sh
Capgo Cloud remains a 1-config-field fallback if self-host becomes a burden
Bundle versioning is decoupled from native CFBundleShortVersionString
06
Hard constraints — must-not-OTA list
These 13 surfaces are forbidden for hot-shipping and must go through a native App Store / Play release. The "no-touch file allow-list" CI gate in deploy-ota.yml enforces every row below; override requires --force-native-only-surface flag plus written reason.