Pear Docs

Deploy your mobile application

Operator how-to for the mobile release flow: build the OTA payload, mobile release lines, and store submissions—the steps that differ from the desktop Foundational Steps.

This is the mobile counterpart to Deploy your application. It covers only what's different for a React Native or Expo app built on pear-mobile.

Steps 0, 1, 2, 5, 6, and 7 of the Foundational Steps—touch and seed, the upgrade link, version, stage, provision, and multisig—are unchanged from desktop. Follow those in Deploy your application; this page covers only what's different: step 3–4, Build the OTA payload, plus mobile-only release-line and store-submission notes.

Before you begin

Need the pear CLI? Install it from install.pears.com, or prefix any command below with npx. See Install & upgrade for details.

Build the OTA payload (steps 3–4)

The package.json/pear.json checklist before a release:

  • version bumped and pear.json updates.minver set for this release
  • upgrade set to the release line's link
  • author, license, description, name, and productName set per brand
  • app.json icons, ios.bundleIdentifier, and android.package set per brand

Two bundles, not one

A mobile release packs two separate bundles that are easy to conflate:

  • The Bare worker bundle—your peer-to-peer and updater logic—packed with bare-pack into src/worker.bundle.js. This is the same bundling step covered in Integrate Pear OTA into an existing mobile app.
  • The Metro bundle—your React Native view code—produced by react-native bundle. This is the actual OTA payload: the file the updater downloads and the one native boot control swaps in.

Only the Metro bundle ships as the OTA payload. The worker bundle is loaded locally by the worklet at every launch; it never travels over the wire as part of an update.

Pack the worker for every host you ship (four here—one per pear build flag below):

bare-pack --host ios-arm64 --host ios-arm64-simulator --host ios-x64-simulator --host android-arm64 --linked --out ./src/worker.bundle.js ./workers/main.js

Produce the Metro bundle per platform. React Native bundles are architecture-agnostic within a platform, so the same iOS output feeds all three --ios-* flags below:

npx react-native bundle --platform ios --dev false --entry-file index.ts --bundle-output out/ios/HelloPear/app.bundle --assets-dest out/ios/HelloPear
npx react-native bundle --platform android --dev false --entry-file index.ts --bundle-output out/android/HelloPear/app.bundle --assets-dest out/android/HelloPear

The HelloPear leaf above is package.json productName, and it must match in three places: the out/<platform>/HelloPear paths here, the directory basename passed to each pear build --<host> flag below, and the name the app passes into the worker (see pear-mobile's name option). pear-build validates the app basename against the package identity, but the worker's name must still match by hand—the updater looks for exactly /by-arch/<host>/app/<productName>.

Assemble the deployment directory with pear build. --config is mandatory for mobile targets—see pear build:

pear build \
  --ios-arm64 ./out/ios/HelloPear \
  --ios-arm64-simulator ./out/ios/HelloPear \
  --ios-x64-simulator ./out/ios/HelloPear \
  --android-arm64 ./out/android/HelloPear \
  --package ./package.json \
  --config ./pear.json \
  --target dist

--config copies pear.json verbatim to dist/pear.json alongside package.json. That's how updates.minver (and any multisig config) travels with the payload; editing the multisig block derives a different production key, editing minver does not.

The resulting deployment directory:

dist/
  package.json
  pear.json
  by-arch/
    ios-arm64/app/HelloPear/app.bundle
    ios-arm64-simulator/app/HelloPear/app.bundle
    ios-x64-simulator/app/HelloPear/app.bundle
    android-arm64/app/HelloPear/app.bundle

hello-pear-react-native wraps the three commands above behind one npm run update script (bundle:bare + bundle:react-native + build in sequence). Use that if you started from the template; the raw commands above are what it runs.

dist/ is now a Deployment Directory in the standard package.json + by-arch/<host>/app shape—stage it exactly as on desktop, dry-run first:

pear stage --dry-run pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o dist
pear stage pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o dist

Release lines on mobile

Release lines work as on desktop, except that upgrade is compiled into the native build: a line is pinned to the build its users have installed, and no OTA can move them to another line.

Release lineNative build its users runOTA source
developmentlocally-built release variantstage link
staginginternally distributed buildstage link
rcTestFlight / internal testingstage link
prereleaseTestFlight / internal testingprovision
productionApp Store / Play Store releasemultisig

As on desktop, the rc line's upgrade points at the production multisig link, so rc and prerelease builds receive no OTAs—each iteration is a new build. A line's minver must never exceed the native version its users run, or every payload on that line is skipped.

Store submissions

Beyond peer-to-peer distribution, native builds still need to reach the App Store and Play Store. This template carries no store configuration beyond app.json (ios.bundleIdentifier, android.package, icons); signing, provisioning profiles, and store metadata are project-specific, and there's no bundled EAS or CI release workflow—build from the generated ios/ and android/ projects with the usual platform tooling. See Submit to app stores for the iOS and Android walkthrough.

Store review can take days, and a payload staged during that window must not require the unreleased native build—see Native release in the upstream README.

Troubleshooting

The app shows its version and nothing else

The worklet died at boot. Check the system log—nothing surfaces in the Metro console. The usual cause is an invalid upgrade link in a build made outside npm run ios / npm run android, which skips the validation check.

The app did not update

  • Was package.json version bumped? An equal version is never selected.
  • Is the upgrade link correct, and is the drive seeded?
  • Was the worker bundle re-run after the worker changed—see Two bundles, not one?
  • Is this a debug build? Debug always loads from Metro.
  • Was the app opened before the seeder came online? Peer lookups repeat roughly every 15 minutes—see Seeder came online after the client.
  • On Android, was the app fully relaunched rather than reloaded? iOS re-reads the bundle URL on every reload; Android captures it when its ReactHost is first created—see Runtime activation.

The app says "Update available on the App Store / Play Store"

The payload's minver is higher than the running native version, so it was skipped by design—see The minver gate.

An update downloaded but the app still runs the old bundle

Native boot selection rejected it. Both app.bundle and package.json must be present under pear-runtime/ota/, and ios/ and android/ must have been regenerated with the current boot-control plugin version.

The updater never sees a peer

pear seed <link> --json prints firewalled and natType; natType: "Random" is symmetric NAT, which defeats holepunching. Seed from a host with a public address or cone NAT.

See also

On this page