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 install2. Create a valid upgrade link
The committed upgrade field is the placeholder pear://<YOUR_KEY_HERE>. Create a real link with pear touch:
pear touchThis 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:bareThis 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 androidBoth 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:androidMap the template
| Path | What it is | Do you edit it? |
|---|---|---|
src/App.tsx | The frontend—starts the worklet, wires the update UI. | Yes—this is your UI. |
workers/main.js | The app logic—a Bare worker that owns the swarm, storage, and updater. | Yes—this is your backend. |
package.json | App metadata, scripts, and the upgrade link. | Yes—branding and release link. |
pear.json | The updates.minver compatibility floor. Ships inside every OTA payload, so a multisig block added here travels with it. | At production time. |
app.json | Expo config—icons, bundle identifiers, and the pear-runtime-react-native config plugin registration. | Yes—brand assets and platform identifiers. |
metro.config.js | Merges the React Native and Expo Metro defaults, used to produce OTA payloads. | Rarely. |
scripts/check-upgrade.js | Validates 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
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:
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:
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:
| Direction | Message | Meaning |
|---|---|---|
| view → worker | pear:applyUpdate | Apply a downloaded update and report back. |
| worker → view | Hello from worker | Sent once when the worker comes up. The template's view ignores it; it's there as a liveness check you can hook. |
| worker → view | updating | An update started downloading. |
| worker → view | updated | The update is staged; ready to apply. |
| worker → view | minver-required | The available update needs a newer native build—see the minver gate. Mobile-only; desktop has no equivalent. |
| worker → view | pear:updateApplied | Reply 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—setname,productName,version, and theupgradepear://link.app.json—replace the icons and splash assets, and setios.bundleIdentifierandandroid.package.pear.json—setupdates.minverwhenever a release changes the native/OTA contract (see theminvergate). The template ships this file withupdates.minveronly; add amultisigblock at production time, and note that editing it derives a different production key while editingminverdoes not.
Where to go next
- Start from a template—all three boilerplates, side by side.
- Start from the hello-pear-electron template—the desktop counterpart.
- Pear Mobile OTA—the full
pear-mobileAPI this template embeds. - Embed Bare in a React Native app—the raw
bare-kitprimitive this template builds on. - One core, many platforms—the pattern behind sharing
workers/main.jsacross every template. - Handle app suspension—keep the worklet in step with the OS lifecycle.
- Type a native RPC bridge—replace raw IPC strings with a typed, schema-generated seam.
- Bundle a Bare app—more on the
bare-packstep behindnpm run bundle:bare.
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.