# Start a mobile app from the hello-pear-react-native template



This is the mobile counterpart to [Start from the hello-pear-electron template](/p2p/getting-started/from-a-template/start-from-hello-pear-electron). 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`](/pear/reference/pear/mobile) connects them.

[`holepunchto/hello-pear-react-native`](https://github.com/holepunchto/hello-pear-react-native) is Holepunch's official React Native template—end-to-end boilerplate for embedding [`pear-mobile`](/pear/reference/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](/p2p/getting-started/from-a-template/start-from-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](/bare/how-to/run-on-native/embed-bare-in-react-native). That how-to shows the raw [`bare-kit`](/bare/reference/bare/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.

<Callout type="warn">
  Both `hello-pear-react-native` and the [`pear-mobile`](/pear/reference/pear/mobile) module it embeds are MVP and experimental—expect the API to keep moving.
</Callout>

<Callout type="info">
  **Need the `pear` CLI?** Install it from **[install.pears.com](https://install.pears.com)**, or prefix any command below with `npx`. See [Install & upgrade](/pear/reference/pear/cli#install) for details.
</Callout>

<Callout type="info">
  **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](/pear/how-to/browse-commands-with-the-interactive-menu).
</Callout>

## Clone and run [#clone-and-run]

### 1. Clone the repository [#1-clone-the-repository]

Clone the repository and install dependencies with the following commands:

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

### 2. Create a valid `upgrade` link [#2-create-a-valid-upgrade-link]

The committed `upgrade` field is the placeholder `pear://<YOUR_KEY_HERE>`. Create a real link with [`pear touch`](/pear/reference/pear/cli#pear-touch-flags):

```bash
pear touch
```

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

```json
"upgrade": "pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o"
```

### 3. Bundle the worker [#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`](https://github.com/holepunchto/bare-pack):

```bash
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 [#4-run-the-app]

```bash
npm run ios
# or
npm run android
```

<Callout type="info">
  Both scripts run `scripts/check-upgrade.js` first, which parses `package.json`'s `upgrade` field with [`pear-link`](https://github.com/holepunchto/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.
</Callout>

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

```bash
npm run production:ios
npm run production:android
```

## Map the template [#map-the-template]

| Path                                                                                                                    | What it is                                                                                                                                         | Do you edit it?                            |
| ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| [`src/App.tsx`](https://github.com/holepunchto/hello-pear-react-native/blob/main/src/App.tsx)                           | The frontend—starts the worklet, wires the update UI.                                                                                              | Yes—this is your UI.                       |
| [`workers/main.js`](https://github.com/holepunchto/hello-pear-react-native/blob/main/workers/main.js)                   | The app logic—a [Bare](/bare/reference/bare/modules) worker that owns the swarm, storage, and updater.                                             | Yes—this is your backend.                  |
| [`package.json`](https://github.com/holepunchto/hello-pear-react-native/blob/main/package.json)                         | App metadata, scripts, and the `upgrade` link.                                                                                                     | Yes—branding and release link.             |
| [`pear.json`](https://github.com/holepunchto/hello-pear-react-native/blob/main/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`](https://github.com/holepunchto/hello-pear-react-native/blob/main/app.json)                                 | Expo config—icons, bundle identifiers, and the [`pear-runtime-react-native`](/pear/reference/pear/mobile-boot-control) config plugin registration. | Yes—brand assets and platform identifiers. |
| [`metro.config.js`](https://github.com/holepunchto/hello-pear-react-native/blob/main/metro.config.js)                   | Merges the React Native and Expo Metro defaults, used to produce OTA payloads.                                                                     | Rarely.                                    |
| [`scripts/check-upgrade.js`](https://github.com/holepunchto/hello-pear-react-native/blob/main/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 [#how-the-pieces-connect]

<Mermaid
  chart="flowchart LR
  app[&#x22;src/App.tsx (React Native view)&#x22;] <-->|&#x22;PearRuntime.run() + FramedStream IPC&#x22;| worker[&#x22;workers/main.js (Bare worklet, via bare-kit)&#x22;]
  worker --> stack[&#x22;Hyperswarm + Corestore + updater&#x22;]"
/>

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 [#where-the-frontend-goes]

Your UI lives in `src/App.tsx`. On mount, it starts the worklet with [`PearRuntime.run`](/pear/reference/pear/mobile#starting-the-worklet-react-native-side) and wraps its `IPC` in a [`FramedStream`](https://www.npmjs.com/package/framed-stream):

```js file=<rootDir>/examples/getting-started/hello-pear-react-native/src/App.tsx#L28-L34 title="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 [#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`](/pear/reference/pear/runtime) instance:

```js file=<rootDir>/examples/getting-started/hello-pear-electron/workers/main.js#L15-L27 title="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](/p2p/reference/helpers/corestore) cores, join [Hyperswarm](/p2p/reference/building-blocks/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](/pear/explanation/storage-and-distribution). For why the logic belongs in a worker rather than the UI, see [Workers](/pear/explanation/workers).

<Callout type="info">
  Upstream now ships this worker as the [`hello-pear-worker`](https://github.com/holepunchto/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.
</Callout>

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

## How to connect them [#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](/pear/reference/pear/mobile#the-minver-gate). Mobile-only; desktop has no equivalent. |
| worker → view | `pear:updateApplied` | Reply once `applyUpdate()` has finished.                                                                                                                      |

## Mobile-only concerns [#mobile-only-concerns]

<Callout type="info">
  **Worklet lifecycle.** The worklet must stop active I/O when the app is backgrounded or the OS force-terminates it. See [Handle app suspension](/bare/how-to/run-on-native/handle-app-suspension) for the suspend/resume patterns.
</Callout>

<Callout type="info">
  **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](/pear/reference/pear/mobile#the-minver-gate). [Deploy your mobile application](/pear/how-to/operate-an-app/manual-deployment/mobile) documents the full flow: the mobile "Build the OTA payload" step in place of desktop's make/`pear build`, plus how it plugs into [Deploy your application](/pear/how-to/operate-an-app/manual-deployment/deployment)'s shared stage/provision/multisig model.
</Callout>

## Customize for your brand [#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](/pear/reference/pear/mobile#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 [#where-to-go-next]

* [Start from a template](/p2p/getting-started/from-a-template)—all the boilerplates, side by side.
* [Start from the hello-pear-electron template](/p2p/getting-started/from-a-template/start-from-hello-pear-electron)—the desktop counterpart.
* [Pear Mobile OTA](/pear/reference/pear/mobile)—the full `pear-mobile` API this template embeds.
* [Embed Bare in a React Native app](/bare/how-to/run-on-native/embed-bare-in-react-native)—the raw `bare-kit` primitive this template builds on.
* [One core, many platforms](/bare/explanation/bare-on-native)—the pattern behind sharing `workers/main.js` across every template.
* [Handle app suspension](/bare/how-to/run-on-native/handle-app-suspension)—keep the worklet in step with the OS lifecycle.
* [Type a native RPC bridge](/bare/how-to/run-on-native/type-a-native-rpc-bridge)—replace raw IPC strings with a typed, schema-generated seam.
* [Bundle a Bare app](/bare/how-to/run-on-native/bundle-a-bare-app)—more on the `bare-pack` step behind `npm run bundle:bare`.

For a real app extending this template, see [`holepunchto/snake-mobile`](https://github.com/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—worth reading if you're about to add real peer-to-peer logic of your own.
