BUILT INTO EVERY GOODSPEED APP
OTA Auto-Update on Launch
On every app launch the template checks for an available EAS Update in the background and silently applies it on next restart, so users always run the latest JS without needing to visit the App Store.
- Tier: Core
- Status: Static
- Always enabled
WHY IT MATTERS
Every shipped mobile app carries the App Store review cycle as a distribution tax. A bug fix that takes half an hour to write can sit in review for one to three days before reaching users. EAS Update sidesteps this entirely for JavaScript changes: the template calls Updates.checkForUpdateAsync() on every cold start, and if a newer bundle is available on the EAS CDN it is fetched silently in the background. The next app restart applies the new bundle. From the user perspective nothing happened except the next launch runs the corrected code. No forced update dialog, no App Store trip, no lost context.
The template goes beyond the bare expo-updates install. It layers a crash-on-launch guard on top of the update check. Before any rendering happens, guardLaunch() in lib/app-update.ts inspects an AsyncStorage sentinel written at the previous boot. If the sentinel is still present and the current bundle is an OTA build (not the embedded binary), the app infers the previous launch crashed before calling markLaunchHealthy(). It reports the event to Sentry and PostHog, then immediately attempts to pull a newer update in case the operator has already shipped a fix. The splash screen is held open via SplashScreen.preventAutoHideAsync() until the check completes, so users never see a flicker between splash and first frame. The combination of silent delivery, crash detection, and self-healing recovery means a bad JS change is caught, reported, and resolved within a single user restart cycle rather than accumulating crash reports for hours.
HONEST LIMITS
When OTA Auto-Update on Launch is the wrong choice
Apps in regulated verticals where the store review gate is itself the compliance checkpoint should disable OTA updates. FDA-cleared medical device apps (Software as a Medical Device, SaMD) are the canonical example: the 510(k) or De Novo clearance covers a specific software version, and distributing a JS update without a new regulatory filing is a compliance violation regardless of whether the change seems minor. Similarly, some financial apps in jurisdictions that require software change-control audit trails may be bound by internal policies that treat the App Store review as a required approval step. In those cases, set runtimeVersion to a value that forces a binary bump on every change, preventing any JS-only OTA path. Apps that run entirely offline with no update infrastructure should also skip the on-launch check. The call fails gracefully and silently, but it still adds a network round-trip attempt on every cold start. If your app is designed for fully air-gapped environments or devices that never connect to the internet, remove the applyOTAIfAvailable call from app/_layout.tsx to avoid the overhead and any associated privacy surface.
Tier: Core · Static
Evaluate your use case
Check whether ota auto-update on launch aligns with your target audience, platform constraints, and regulatory environment before enabling it.
Audit the config
This feature is always enabled. If you need to disable it, remove the corresponding template files after generation.
Seek alternatives
If the built-in implementation does not fit, the generated codebase is standard React Native + Expo code. Any library in the Expo ecosystem can replace the default.
APPS USING THIS FEATURE
Every generated Goodspeed app includes ota auto-update on launch. Browse the ideas catalog to see apps across all categories that ship with this feature wired in.
CAPABILITIES
OTA Auto-Update on Launch capability breakdown
Concrete dimensions of what the built-in ota auto-update on launch implementation covers. These reflect the actual template code, not a marketing summary.
| Item | Description | Strength |
|---|---|---|
| Update delivery | JS bundles are published to the EAS CDN and fetched over HTTPS on every cold start. Expo's global edge network serves the bundle from the nearest PoP; no custom CDN configuration is required. | EAS CDN (Expo global edge) |
| Check trigger | checkForUpdateAsync is called once per cold start inside the guardLaunch flow. The check completes before SplashScreen.hideAsync fires, so the first rendered frame always reflects the latest available published bundle. | Cold start (every launch) |
| Crash recovery | guardLaunch detects a crash-on-launch from an OTA bundle via an AsyncStorage sentinel written before each render. It reports to Sentry and PostHog and attempts to self-heal by pulling the newest available update before the app renders. | Automatic detect and self-heal |
| Dev/local behavior | Updates.isEnabled returns false in Expo Go and in local development builds, so the check is a silent no-op. The feature only activates in EAS-built production and preview binaries with a valid runtimeVersion. | No-op in Expo Go and local dev |
| Channel targeting | The active EAS Update channel is read from EXPO_PUBLIC_RELEASE_CHANNEL via gas.config releaseChannels.current. Preview builds consume the preview channel; production builds consume production. Different audiences receive different JS bundles from the same binary. | Per build profile via EXPO_PUBLIC_RELEASE_CHANNEL |
COMMON QUESTIONS
Does OTA update work on both iOS and Android?
Yes. EAS Update delivers JavaScript bundles over HTTPS to both platforms. The expo-updates runtime is embedded in the native binary, so as long as the binary and the JS bundle are SDK-compatible (same Expo SDK major version, no new native modules added since the binary was built) the update applies cleanly on both platforms. A new binary release to the App Store and Play Store is required when you add a new native module, change native app.json config keys such as bundle identifier or permissions, or upgrade to a new Expo SDK major version. For the common case of fixing a UI bug, updating copy, correcting a data-fetching logic issue, or changing business rules, OTA covers it without any binary change and without waiting for store review. The practical rule is: if the change touches the Objective-C or Kotlin native layer, you need a binary release; if it stays in the JavaScript layer, OTA handles it.
What happens if the device is offline when the app launches?
The checkForUpdateAsync call throws a network error, the catch block in applyOTAIfAvailable suppresses it silently, and the app continues loading with whatever bundle is already installed on the device. Offline launches are indistinguishable from normal launches from the user perspective. The splash screen still hides on schedule because the update check errors out immediately rather than hanging indefinitely. The crash-on-launch guard still runs correctly because it reads from AsyncStorage, which is a local operation with no network dependency. Crash detection and the sentinel tracking both work fully offline. Once the device comes back online, the next cold start will pick up any pending update that was published while the device was offline. There is no queued retry mechanism. Each launch is independent, which keeps the logic simple.
Can I target a specific subset of users with an update, for example only beta testers?
Yes. EAS Update channels map to EAS build profiles. The gas-template reads the active channel from the EXPO_PUBLIC_RELEASE_CHANNEL environment variable, which is set in your EAS build profile and surfaced in the app via gas.config through the releaseChannels.current field. A build distributed to TestFlight can be pinned to the preview channel while production App Store builds consume the production channel. This means you can publish a JS update targeting only the preview channel, verify it with beta testers, and then promote the same exact bundle to the production channel without rebuilding or resubmitting to the store. Production users remain on the last production-channel publish until you explicitly promote via the EAS dashboard.
What if an OTA update itself introduces a crash that prevents the app from loading?
The crash-on-launch guard in lib/app-update.ts handles this case directly. On every launch the app writes a timestamp sentinel to AsyncStorage before rendering. If the app crashes before markLaunchHealthy() clears the sentinel (the timer fires four seconds after the root component mounts), the sentinel remains on the next boot. When guardLaunch() finds the sentinel and confirms that Updates.isEmbeddedLaunch is false (meaning the current bundle is an OTA build, not the original binary), it reports a crash-on-launch event to both Sentry and PostHog so the operator is alerted within seconds of the first affected user restarting. It then checks for a newer update immediately. If the operator has already shipped a fix, the fix is fetched and applied before the app renders anything. If no fix is available yet, the app continues loading and the operator has full telemetry to act on. Note that expo-updates v55 removed rollbackToEmbeddedAsync, so client-side rollback to the original binary is not supported. The operator recovery path is to publish a corrected update or use the EAS dashboard rollback to promote a prior known-good bundle on the channel.
GET IT BUILT INTO YOUR APP