LogoPear Docs

Mobile OTA Boot Control

The pear-runtime-react-native Expo config plugin: how it chooses between a native build's bundled JS and a downloaded Pear OTA update.

Mobile OTA Boot Control

Mobile OTA boot control is provided by the pear-runtime-react-native package, an Expo config plugin. It owns exactly one thing: patching a React Native app's generated native projects so a release build boots either the JS bundle shipped in the binary or a newer one downloaded via Pear Mobile OTA.

pear-runtime-react-native is MVP and experimental—expect the API to keep moving.

It has no runtime API and nothing to import in app code. The runtime side—storage, the Bare worklet, the update flow—is pear-mobile. Install both together:

npm install pear-runtime-react-native pear-mobile react-native-bare-kit
npx expo install expo-build-properties

Expo setup

The plugin requires Expo and Prebuild—there's no autolinking path and no runtime patching, only generate-then-patch. Register it in app.json:

{
  "expo": {
    "plugins": [
      "pear-runtime-react-native",
      [
        "expo-build-properties",
        { "android": { "minSdkVersion": 29 } }
      ]
    ],
    "ios": { "bundleIdentifier": "com.example.app" },
    "android": { "package": "com.example.app" }
  }
}

Then generate the native projects:

npx expo prebuild

The plugin edits two functions: bundleURL() in AppDelegate.swift, and the jsBundleFilePath passed into ExpoReactHostFactory.getDefaultReactHost() in MainApplication.kt. Re-run with --clean to regenerate already-patched files.

For a project without Expo, the same logic has to be applied by hand—see Plain React Native below.

Boot control

A release build loads the downloaded OTA update only if the version in the OTA's package.json is newer than the installed app version; otherwise it loads the bundle shipped in the binary. Debug builds always load from Metro—OTA behavior can only be verified in a release build.

PlatformOTA folderCompared against
iOS<Application Support>/pear-runtime/otaCFBundleShortVersionString
Android<filesDir>/pear-runtime/otathe package versionName

Comparison is plain SemVer 2.0.0, no extra rules. Both sides must be valid SemVer—anything the spec rejects counts as not newer, so a bad version string just means the app keeps booting the binary's bundle rather than erroring.

An OTA may change JavaScript and bundled worker code only when that code stays compatible with the exact native modules, native ABI, configuration, and assets shipped in the installed store build.

Version synchronization

package.json's version is the single source of truth for four values that all have to move together: the OTA manifest version this plugin compares, Expo's version, CFBundleShortVersionString, and versionName. An app.config.js keeps the native side in sync, since Expo writes its version into both native projects during prebuild:

const app = require('./app.json')
const { version } = require('./package.json')

module.exports = { ...app.expo, version }

Versions must be valid SemVer and strictly monotonic across native and OTA releases combined: every OTA version must be greater than the installed native version and every previously published OTA, and every new native release must be greater than every previously published OTA. Never reuse or decrease a published version.

Two independent gates enforce different halves of that rule, and they're worth keeping apart:

  • pear.json updates.minver, checked by pear-mobile before a payload is downloaded and applied. It stops an OTA from reaching a native build too old for it—the forward direction.
  • Boot control, in this package, runs on every launch and compares only the installed OTA manifest version against the native version. It never reads pear.json, so minver plays no part in choosing which bundle boots.

Runtime activation

Applying an update writes the new bundle and manifest into pear-runtime/ota, but doesn't switch the running app over—activation happens at the next native bundle selection, and the two platforms differ on when that is:

  • iOS re-reads bundleURL() on reload, so a JavaScript reload can pick up a freshly applied OTA.
  • Android captures jsBundleFilePath when the React host is created and caches it, so a JavaScript reload generally reuses the old path—the update takes effect after a full process restart.

Treat a full restart as the requirement on both platforms.

What the plugin will and won't touch

The plugin only rewrites the two functions named above, wrapping what it writes in a marker comment (pear-runtime-react-native OTA v3 / ... end). A later prebuild reads that marker before touching anything:

What it findsWhat it does
No markerLinks the plugin's code.
This version's markerLeaves it alone.
Another version's marker, presentReplaces the whole block.
Another version's marker, removedWarns, changes nothing.

Editing the generated code is fine as long as the marker comment stays—removing it hands ownership of that block to the project, and prebuild will only warn that a newer version wasn't linked (a clean --clean prebuild replaces it either way).

Adopting this plugin in a project that already has its own code in bundleURL() or getDefaultReactHost() overwrites it on the next prebuild—the plugin can't tell a hand-written implementation from Expo's generated one. An app should have exactly one owner of bundle selection; this conflicts with anything else that also claims it, including Expo Updates or another OTA system.

Plain React Native

The plugin only works with Expo—it runs as a config mod, and the Android patch looks for ExpoReactHostFactory. Without Expo, apply the same logic by hand:

  • iOS: override bundleURL() in AppDelegate. Return <Application Support>/pear-runtime/ota/app.bundle if it exists and its package.json version is newer than CFBundleShortVersionString, otherwise return the shipped main.jsbundle.
  • Android: override getJSBundleFile() on the ReactNativeHost. Return <filesDir>/pear-runtime/ota/app.bundle under the same condition, otherwise null.

The Swift and Kotlin the plugin generates is available for reference in lib/ota-templates.js.

See also

On this page