LogoPear Docs

Start from the hello-pear-react-native template

An alternative getting started path: clone the hello-pear-react-native boilerplate and learn where the React Native view lives, where the app logic lives, and how pear-mobile wires a Bare worklet and over-the-air updates into an Expo app.

This is the mobile counterpart to Start from the hello-pear-electron template. Instead of an Electron window, you start from a finished Expo/React Native boilerplate and learn where your view lives, where your peer-to-peer logic lives, and how pear-mobile connects them.

holepunchto/hello-pear-react-native is Holepunch's official React Native template—end-to-end boilerplate for embedding pear-mobile into an Expo app and deploying peer-to-peer application updates. It's a clone-first tour, the same shape as the hello-pear-electron tour: where to put your UI, where to put your peer-to-peer logic, and how the two talk.

This is a different page from Embed Bare in a React Native app. That how-to shows the raw bare-kit primitive—a Worklet and an IPC channel, nothing else. This page is the production-shaped template built on top of it: over-the-air updates, application storage, and a staged deployment pipeline, the mobile equivalent of what hello-pear-electron's page is to the desktop primitives.

Both hello-pear-react-native and the pear-mobile module it embeds are MVP and experimental—expect the API to keep moving.

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

New to the CLI? Run pear --menu (Pear 3.2.0+) to browse every command from a filterable list and fill in its flags as a form, instead of memorizing them. See Browse commands with the interactive menu.

Clone and run

1. Clone the repository

Clone the repository and install dependencies with the following commands:

git clone https://github.com/holepunchto/hello-pear-react-native
cd hello-pear-react-native
npm install

The committed upgrade field is the placeholder pear://<YOUR_KEY_HERE>. Create a real link with pear touch:

pear touch

This prints a link, for example: pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o. Set it in package.json:

"upgrade": "pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o"

3. Bundle the worker

Unlike the desktop templates, mobile has one extra step before the app can run. Pack the Bare worker into a worklet bundle with bare-pack:

npm run bundle:bare

This writes src/worker.bundle.js, imported directly by src/App.tsx. Rerun it after every change to workers/main.js or a package it imports—nothing rebuilds it automatically, and a stale bundle fails silently: the app still boots and looks normal, it's just running old worker code.

4. Run the app

npm run ios
# or
npm run android

Both scripts run scripts/check-upgrade.js first, which parses package.json's upgrade field with pear-link and refuses to launch on an invalid link—so a leftover placeholder fails fast with a clear message instead of silently killing the worklet at startup. Requires Node.js ≥ 20.19.4, plus Xcode ≥ 26.2 for iOS (iOS ≥ 15.1) and Android ≥ API 29.

Development builds always load JavaScript from Metro, so the update path itself isn't exercised. Use the release variants for that:

npm run production:ios
npm run production:android

Map the template

PathWhat it isDo you edit it?
src/App.tsxThe frontend—starts the worklet, wires the update UI.Yes—this is your UI.
workers/main.jsThe app logic—a Bare worker that owns the swarm, storage, and updater.Yes—this is your backend.
package.jsonApp metadata, scripts, and the upgrade link.Yes—branding and release link.
pear.jsonThe updates.minver compatibility floor. Ships inside every OTA payload, so a multisig block added here travels with it.At production time.
app.jsonExpo config—icons, bundle identifiers, and the pear-runtime-react-native config plugin registration.Yes—brand assets and platform identifiers.
metro.config.jsMerges the React Native and Expo Metro defaults, used to produce OTA payloads.Rarely.
scripts/check-upgrade.jsValidates package.json#upgrade before npm run ios/android hand off to Expo.Rarely.

The two pieces you care about day to day are src/App.tsx (view) and workers/main.js (logic).

How the pieces connect

PearRuntime.run() + FramedStream IPC src/App.tsx (React Native view) workers/main.js (Bare worklet, via bare-kit) Hyperswarm + Corestore + updater

Desktop's hello-pear-electron splits into three processes—renderer, Electron main, and Bare worker—connected by a preload bridge and an IPC proxy. Mobile collapses that into two: the React Native view starts the worklet directly and talks to it over one IPC duplex. There's no separate main process to proxy through.

Where the frontend goes

Your UI lives in src/App.tsx. On mount, it starts the worklet with PearRuntime.run and wraps its IPC in a FramedStream:

src/App.tsx
    const IPC = PearRuntime.run('/worker.bundle', bundle, [
      (!__DEV__).toString(),
      version,
      upgrade,
      appName
    ])
    const pipe = new FramedStream(IPC)

There is no framework requirement beyond Expo/React Native itself—the template ships plain View/Text/Pressable, but bring your own component library. The only rule is: your UI reaches the peer-to-peer core only through this one IPC pipe.

Where the app logic goes

Everything peer-to-peer lives in workers/main.js, which runs in Bare (not Node). The host passes the runtime configuration as positional arguments; the worker reads them (with an argv helper for cross-platform compatibility) and constructs the pear-runtime instance:

workers/main.js
const updaterConfig = {
  updates: argv(0) !== 'false',
  version: argv(1),
  upgrade: argv(2),
  name: argv(3),
  dir: argv(4) || dir.persistent(), // argv[4] is undefined in mobile
  app: argv(5) // argv[5] is undefined in mobile
}

const pipe = new FramedStream(Bare.IPC)
const store = new Corestore(path.join(updaterConfig.dir, 'pear-runtime', 'corestore'))
const swarm = new Hyperswarm()
const pear = new PearRuntime({ ...updaterConfig, swarm, store })

This is where you add your Corestore cores, join Hyperswarm topics, and run your protocols. Use pear.storage as the storage root so your data lands in the same per-app directory Pear manages—see Storage and distribution. For why the logic belongs in a worker rather than the UI, see Workers.

Upstream now ships this worker as the hello-pear-worker package—the template's workers/main.js is just require('hello-pear-worker'). The code above is that worker inlined so you can see what it does; write your own peer-to-peer logic in workers/main.js the same way.

This is the same worker every hello-pear-* template uses—see One core, many platforms for why that portability is the point. On mobile, hello-pear-worker's own require('pear-runtime') resolves to pear-mobile instead of the desktop package; nothing in workers/main.js itself has to change. Remember to re-run npm run bundle:bare after editing it.

How to connect them

There's no bridge or preload layer here—src/App.tsx talks to the worker directly over the FramedStream-framed pipe, in plain strings:

DirectionMessageMeaning
view → workerpear:applyUpdateApply a downloaded update and report back.
worker → viewHello from workerSent once when the worker comes up. The template's view ignores it; it's there as a liveness check you can hook.
worker → viewupdatingAn update started downloading.
worker → viewupdatedThe update is staged; ready to apply.
worker → viewminver-requiredThe available update needs a newer native build—see the minver gate. Mobile-only; desktop has no equivalent.
worker → viewpear:updateAppliedReply once applyUpdate() has finished.

Mobile-only concerns

Worklet lifecycle. The worklet must stop active I/O when the app is backgrounded or the OS force-terminates it. See Handle app suspension for the suspend/resume patterns.

OTA differs from desktop. A mobile OTA payload is a JavaScript bundle per host, not a set of native distributables, and native/OTA releases share one SemVer sequence gated by pear.json's updates.minver—see Pear Mobile OTA. The full staging, provisioning, and multisig ceremony is documented in the upstream hello-pear-react-native README—it reuses Deploy your application's model but with a different build step, so that page's distributables flow doesn't apply here verbatim.

Customize for your brand

Before you ship:

  • package.json—set name, productName, version, and the upgrade pear:// link.
  • app.json—replace the icons and splash assets, and set ios.bundleIdentifier and android.package.
  • pear.json—set updates.minver whenever a release changes the native/OTA contract (see the minver gate). The template ships this file with updates.minver only; add a multisig block at production time, and note that editing it derives a different production key while editing minver does not.

Where to go next

For a real app extending this template, see Add custom peer-to-peer logic to a React Native app—it walks through holepunchto/snake-mobile, a production-shaped P2P multiplayer game built on hello-pear-react-native. Its worker keeps its own custom Hyperswarm protocol in workers/main.js instead of hello-pear-worker, layered on the same updater scaffolding.

On this page