# Pear import { Tabs, Tab } from 'fumadocs-ui/components/tabs' import { Cards, Card } from 'fumadocs-ui/components/card' Pear is an installable peer-to-peer runtime, development, and deployment platform. Build, share, and extend unstoppable, zero-infrastructure P2P apps for mobile, desktop, and terminal. Welcome to the Internet of Peers.   *– Holepunch, the P2P Company* Install Pear [#install-pear] Get the [`pear`](/reference/pear/cli) CLI from **[install.pears.com](https://install.pears.com)**: ```sh curl https://install.pears.com/pear.sh | sh ``` ```powershell irm https://install.pears.com/pear.ps1 | iex ``` ```sh npx pear ``` ```sh docker run -it --rm tetherto/pear ``` Follow the `PATH` instructions the installer prints, then run `pear` directly. Full details, upgrade instructions, and the npm-global alternative are on [Install & upgrade](/reference/pear/cli#install). The [`tetherto/pear`](https://hub.docker.com/r/tetherto/pear) image runs Pear inside Ubuntu with `pear` and `pear-install` ready to go—a low-risk way to try out Pear apps without installing anything locally. Boilerplates & key docs [#boilerplates--key-docs] Starter paths and the most relevant docs for building and shipping a Pear desktop app (macOS / Linux / Windows): The upstream Electron template used by the team is `holepunchto/hello-pear-electron` on GitHub (clone there for a ready-made repo; use the links above for explanations and procedures). How-to guides by task [#how-to-guides-by-task] Goal-oriented recipes, grouped by task. **Releasing and distributing your app** lives here too—shipping and updating is a how-to, spanning [manual deployment](/how-to/operate-an-app/manual-deployment), [multisig](/how-to/operate-an-app/multisig), [build & package](/how-to/operate-an-app/build-and-package), and [CI](/how-to/operate-an-app/github-actions); the why is in [Release pipeline](/explanation/deployment-releasing-apps-p2p). Module catalog [#module-catalog] The full list of `pear-*` and `bare-*` modules—application libraries, UI libraries, common libraries, developer libraries, integration libraries—lives at **[Reference → Modules](/reference/modules/pear-modules)** and **[Reference → Bare modules](/reference/modules/bare-modules)**. Building-block libraries ([Hypercore](/reference/building-blocks/hypercore), [Hyperbee](/reference/building-blocks/hyperbee), [Hyperdrive](/reference/building-blocks/hyperdrive), …), helpers ([Corestore](/reference/helpers/corestore), [Localdrive](/reference/helpers/localdrive), …), and CLI tools have full reference pages under **[Reference](/reference)**. The runtime underneath has its own reference—the [Bare runtime API](/reference/bare/runtime), the [`bare` CLI](/reference/bare/cli), and [Bare Kit](/reference/bare/bare-kit) for native embedding. # Availability and blind peering Peer-to-peer availability is not automatic. A new user can only download data that at least one online peer already holds. Pear applications therefore need an explicit strategy for [data availability](#data-availability) and [application availability](#application-availability). This page covers seeding, when it falls short, and **blind peering**—a service that replicates cores on your behalf without reading their contents. For the general distribution model, see [Storage and distribution](/explanation/storage-and-distribution). Two availability problems [#two-availability-problems] Data availability [#data-availability] Data availability means the [Hypercores](/reference/building-blocks/hypercore), [Autobases](/reference/building-blocks/autobase), or drives your application reads and writes remain reachable on the swarm. Chat history, shared documents, and room state all depend on someone seeding the relevant cores. Application availability [#application-availability] Application availability means the Pear app itself—its metadata core and content [Hyperdrive](/reference/building-blocks/hyperdrive)—can be discovered and installed. A `pear://` link is stable, but the bytes behind it still have to exist on at least one peer. Both problems share the same underlying constraint: replication requires a live source. Seeding with pear seed [#seeding-with-pear-seed] The simplest way to keep an application bundle online is [seeding](#seeding-with-pear-seed), analogous to seeding a torrent. Running `pear seed pear://` keeps the underlying cores announced and available for download. ```bash pear seed pear:// ``` The process must stay running while you want to act as a guaranteed source. Anyone can open the link, but a first-time user needs at least one seeder with a complete copy. See [Storage and distribution](/explanation/storage-and-distribution#how-distribution-works) for how staging, seeding, and lazy replication fit together—this page does not repeat that model. Seeding works well when you control an always-on machine and the cores you care about are ones you already possess. It has two practical limits: 1. **Operational overhead**—You need a running `pear` (or equivalent replicator) per application you seed. 2. **Scope**—You can only seed cores you have locally. A private room or direct-message thread you are not a member of cannot be seeded from your node. When authors go offline and no participant keeps seeding, data can become temporarily unreachable even though the cryptographic keys still exist. Blind peering [#blind-peering] A **blind peer** is a dedicated replicator that stores and serves Hypercores **without decrypting or interpreting** their contents. Blind peering is one way of **putting the server in serverless**: always-on reachability like a hosted service, but the operator never holds the plaintext. It joins swarms on request and caches blocks so other peers can find a source. Blind peering separates "make this core findable" from "I personally hold the plaintext." The stack has three parts: 1. **[blind-peer](https://github.com/holepunchto/blind-peer)**—server library that accepts peering requests and replicates cores. 2. **[blind-peering](https://github.com/holepunchto/blind-peering)**—client library used inside applications to register cores or Autobases with a blind peer. 3. **[blind-peer-cli](https://github.com/holepunchto/blind-peer-cli)**—CLI that runs a blind peer as a standalone server (provides the `blind-peer` command). Running a blind peer server [#running-a-blind-peer-server] Install the CLI and start the server: ```bash npm i -g blind-peer-cli blind-peer ``` On startup the process logs (as ndjson) a `Listening at ` line—that public key is the blind peer's identity, and clients use it as one of their configured `keys`. The default disk budget is 100 GB; cap it with `-m ` (megabytes). For production, run `blind-peer` under a service manager. Example systemd unit: ```toml [Unit] Description=Blind Peer After=network.target [Service] ExecStart=/usr/local/bin/blind-peer -m 10000 --trusted-peer Restart=always RestartSec=10 [Install] WantedBy=multi-user.target ``` Pass `--trusted-peer ` to authorize a specific peer to announce on the blind peer (set `announce: true` on its requests). The public key here is the identity of the peer that will make those requests—the `Listening at ` value logged by that peer's own swarm node. Registering cores from an application [#registering-cores-from-an-application] Inside a Pear worker, wire the client to your existing [Hyperswarm](/reference/building-blocks/hyperswarm) and [Corestore](/reference/helpers/corestore): ```js import BlindPeering from 'blind-peering' import Hyperswarm from 'hyperswarm' import Corestore from 'corestore' const store = new Corestore(Pear.config.storage) const swarm = new Hyperswarm() swarm.on('connection', (conn) => store.replicate(conn)) const BLIND_PEER_KEYS = [''] const blinds = new BlindPeering(swarm.dht, store.namespace('blind-peering'), { keys: BLIND_PEER_KEYS }) // Ask the blind peers to keep a single Hypercore available… await blinds.addCore(core) // …or a whole Autobase (all of its writer and view cores). await blinds.addAutobase(base) ``` `addCore` / `addAutobase` connect to the closest configured blind peers and request that they replicate and seed the given cores—without the blind peer ever being able to read them. Consider letting users configure which blind peers they trust—default infrastructure may not match privacy or jurisdiction requirements (for example location-sensitive data). Finding the keys to keep available [#finding-the-keys-to-keep-available] `pear info pear://` prints the keys behind an application bundle: ```bash pear info pear:// ``` The `project` and `content` rows identify the metadata core and content drive. To keep them available, open the corresponding cores in a replicator process and register them with the client library shown above—`blinds.addCore(core)` for a single [Hypercore](/reference/building-blocks/hypercore), `blinds.addAutobase(base)` for an [Autobase](/reference/building-blocks/autobase) and all of its writer and view cores. Trusted peers and discovery keys [#trusted-peers-and-discovery-keys] When a blind peer runs with `--trusted-peer`, it recognizes seed requests signed by that key and joins the swarm for the requested core. That is more targeted than broadcasting blindly: peers looking for a specific core use its **discovery key** as a [Hyperswarm](/reference/building-blocks/hyperswarm) topic, so participants in that topic are likely to hold or want the same data. Blind peering does not replace end-to-end encryption or capability checks on the cores themselves; it only improves **reachability** by keeping an always-on replica on the network. See also [#see-also] * [Storage and distribution](/explanation/storage-and-distribution)—on-disk layout and the `pear seed` distribution model. * [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment)—staging and announcing releases. * [Peer-to-peer, demystified](/explanation/peer-to-peer-demystified)—how peers find one another on the swarm. * [Autobase reference](/reference/building-blocks/autobase)—multi-writer cores often registered with blind peering. * [Hyperswarm reference](/reference/building-blocks/hyperswarm)—topic-based discovery blind peers participate in. # One core, many platforms A peer-to-peer app has two very different halves. 1. One is the **core**: swarm identity, storage, replication, and the protocol logic—code that should be identical on every device. 2. The other is the **UI**: windows, views, and gestures—code that is necessarily different on a desktop, a phone, and a terminal. Bare's answer is to keep the core in one place and let only the UI change. This page explains that pattern and the machinery that connects the two halves. For the runtime itself, see [Inside Bare](/explanation/bare-runtime); for where this sits in the wider picture, see [How Pear and Bare fit together](/explanation/pear-and-bare). The shape: shell, seam, core [#the-shape-shell-seam-core] A cross-platform Bare app has three parts: * The **native shell** is written in the platform's own language and owns nothing but presentation. * The **core** runs as a Bare *worklet*—an isolated Bare thread—and owns everything peer-to-peer: * it holds the swarm identity * opens the [Corestore](/reference/helpers/corestore) * runs [Hyperswarm](/reference/building-blocks/hyperswarm) and [HyperDHT](/reference/building-blocks/hyperdht) * handles the Noise-encrypted connections to other peers. * The shell never imports a networking library * The core never imports a UI framework * Between them is a [**typed RPC seam**](#the-typed-rpc-seam) Worklets carry the core [#worklets-carry-the-core] [`bare-kit`](/reference/bare/bare-kit) is the toolkit that runs a Bare core inside a native app. It exposes a web-worker-like API for starting and managing isolated Bare threads—called *worklets*—each with an IPC channel to the host. * On iOS you drive a worklet from Objective-C (Swift interop is available through the standard language bridge); * on Android, from Java (Kotlin interop is available through the standard language bridge); * from React Native or Expo, [`react-native-bare-kit`](/reference/bare/bare-kit) gives you the same `Worklet` and `IPC` objects in JavaScript; * The worklet honours the Bare [lifecycle](/explanation/bare-runtime#the-lifecycle), so the host can suspend and resume the core in step with the OS's app-lifecycle rules. Because the worklet is just Bare, the core you ship to mobile is the *same JavaScript* you'd run in a desktop [worker](/explanation/workers) or a standalone terminal binary. Write it once; embed it everywhere. The typed RPC seam [#the-typed-rpc-seam] The shell and the core could exchange raw bytes over the IPC pipe, but that pushes framing and parsing into hand-written code on both sides. The Bare RPC ecosystem replaces those raw bytes with **typed methods generated from a shared schema**: * [`hyperschema`](https://github.com/holepunchto/hyperschema) defines versioned, append-only data structures and generates [`compact-encoding`](/reference/helpers/compact-encoding) codecs for them. * [`bare-rpc`](/reference/modules/bare-modules) frames requests and replies over a duplex stream, with a unique command number per method. * Code generators turn the schema into typed bindings for each language. JavaScript bindings are generated today, and a Swift toolchain (`hyperschema-swift`, `bare-rpc-swift`, `compact-encoding-swift`, `hrpc-swift`) produces wire-compatible Swift structs and a typed RPC class, with C and Kotlin generators following. The seam supports the RPC patterns a real app needs: * **unary** request/response, * **send-only** events, * **response-stream** (the handler writes a sequence of chunks), * **request-stream** (the caller streams input), and * **duplex** (both at once). Update the schema, regenerate, and both sides get the new types—no hand-maintained parsing, and a compiler error if the shell and core drift apart. One core, many platforms [#one-core-many-platforms] Put the pieces together and the payoff is structural. * The JavaScript core is portable and unchanged across devices. * Each platform adds two cheap, generated or layout-only pieces: a native shell and the bindings for the seam. * Swapping from iOS to Android to desktop changes the shell and the generated bindings—never the protocol logic. This is the architecture behind [Keet's](https://keet.io) identical behaviour on phones, laptops, and terminals: one core, several UIs. The desktop ([Electron](/explanation/pear-desktop-architecture)) and terminal ([standalone Bare binary](/getting-started/from-a-template/start-from-hello-pear-bare)) hosts ship today. On mobile, [`pear-runtime`](/reference/pear/runtime) itself targets desktop; its mobile counterpart is [`pear-mobile`](https://www.npmjs.com/package/pear-mobile), the embeddable Pear runtime for mobile applications—or wire the worklet and updates yourself with `bare-kit`. Common questions [#common-questions] Does Bare run on iOS and Android? [#does-bare-run-on-ios-and-android] Yes. `bare-kit` embeds a Bare worklet in native iOS and Android apps today, and `react-native-bare-kit` does the same for React Native and Expo. The [`pear-runtime`](/reference/pear/runtime) equivalent for mobile is [`pear-mobile`](https://www.npmjs.com/package/pear-mobile) (noted earlier on this page). How does native code talk to the JavaScript core? [#how-does-native-code-talk-to-the-javascript-core] Through an IPC channel between the host and the worklet. You can write bytes directly, or—recommended—put a [typed RPC seam](#the-typed-rpc-seam) on top so both sides call generated, type-checked methods instead. How is this different from React Native's own JavaScript engine? [#how-is-this-different-from-react-natives-own-javascript-engine] React Native runs your UI's JavaScript on the main JS thread. A Bare worklet is a *separate*, isolated runtime for your peer-to-peer core, running off the UI thread with its own module system and native addons. The two communicate over IPC; the worklet keeps networking and storage out of the UI's way. Do I have to write the protocol twice—once in JS, once in Swift? [#do-i-have-to-write-the-protocol-twiceonce-in-js-once-in-swift] No. You define the schema once with `hyperschema` and generate bindings for each language. The Swift, JavaScript (and forthcoming C/Kotlin) sides are all generated from—and wire-compatible with—that single source. See also [#see-also] * [`bare-kit` reference](/reference/bare/bare-kit)—the Worklet and IPC API for iOS, Android, and React Native. * [Embed Bare in a React Native app](/how-to/run-on-native/embed-bare-in-react-native)—start a worklet and exchange messages. * [Type a native RPC bridge](/how-to/run-on-native/type-a-native-rpc-bridge)—generate a typed seam with `hyperschema` and `bare-rpc`. * [Bundle a Bare app](/how-to/run-on-native/bundle-a-bare-app)—produce an embeddable bundle or a standalone binary. * [Workers](/explanation/workers)—the same host-and-core split on the desktop. * [Inside Bare](/explanation/bare-runtime)—the runtime the worklet is an instance of. # Inside Bare [Bare](https://github.com/holepunchto/bare) is the JavaScript runtime every Pear app runs on. Like Node.js, it gives you an asynchronous, event-driven environment for writing applications in JavaScript. Unlike Node.js, it treats **embedding** and **cross-device support** as core use cases—it aims to run just as well inside a phone app as on a laptop or a server. This page explains what that buys you and why the runtime is shaped the way it is. * For the exact API, see the [`Bare` runtime reference](/reference/bare/runtime). * For where Bare sits relative to Pear, see [How Pear and Bare fit together](/explanation/pear-and-bare). What Bare adds, and what it leaves out [#what-bare-adds-and-what-it-leaves-out] Bare is built on two C libraries: [`libjs`](https://github.com/holepunchto/libjs), which provides low-level bindings to V8 (or QuickJS, via [`libqjs`](https://github.com/holepunchto/libqjs)) in an engine-independent way, and [`libuv`](https://github.com/libuv/libuv), which provides the asynchronous I/O event loop. On top of those primitives Bare adds only a few missing pieces: 1. A **module system** supporting both CommonJS and ESM, with bidirectional interoperability between the two. 2. A **native-addon system** supporting both statically and dynamically linked addons. 3. **Lightweight threads** with synchronous joins and `SharedArrayBuffer` support. Everything else is left to userland. There is no built-in `fs`, no built-in `http`, no bundled standard library—those live in installable [`bare-*` modules](/reference/modules/bare-modules) you add only when you need them. The runtime itself stays succinct and, well, *bare*. That minimalism is a deliberate "less is more" stance, and it pays off in two ways. Bundles only carry the modules an app actually declares, so they stay small enough to embed on a phone. And because the standard library isn't baked into the runtime, you can upgrade Bare without being forced to refactor dependencies in lockstep. Engine independence [#engine-independence] By abstracting the JavaScript engine behind the `libjs` ABI and platform I/O behind `libuv`, Bare lets a native addon run on *any* engine that implements the `libjs` ABI and *any* system `libuv` supports. In practice that means Bare can be compiled against V8, through [`libqjs`](https://github.com/holepunchto/libqjs), QuickJS, or through [`libjsc`](https://github.com/holepunchto/libjsc), JavaScriptCore — useful on platforms like iOS where JSC is the sanctioned engine. The addon you wrote against Bare doesn't change. The lifecycle [#the-lifecycle] A server runtime can assume it owns the process and runs until it's killed. A runtime embedded in a mobile app can't: the operating system suspends apps that move to the background and may reclaim them entirely. Bare models this explicitly with a process lifecycle that an embedder — or your JavaScript — can drive. Calling `Bare.suspend()` emits a `suspend` event, signalling that outstanding work (network activity, file access) should be deferred or paused. When the loop runs out of work it emits `idle` and blocks rather than exiting, keeping the process alive but quiet. `Bare.resume()` emits `resume` and lets the loop continue; `Bare.wakeup()` allows a bounded burst of work (**awake** state) during suspension before the process settles back to idle. This is what lets a peer-to-peer core behave correctly when a phone locks the screen: it parks its sockets on `suspend`, sits quietly at `idle`, and picks back up on `resume`—instead of being killed mid-replication. The [full state machine and event list](/reference/bare/runtime#lifecycle) is in the reference. Common questions [#common-questions] Is Bare a fork of Node.js? [#is-bare-a-fork-of-nodejs] No. Bare is a separate runtime built on `libjs` and `libuv`. The surface looks familiar—asynchronous, event-driven, module-based—but Bare drops Node's server-oriented assumptions and its bundled standard library. The [`bare-node`](https://github.com/holepunchto/bare-node) shim maps many Node.js builtins onto their `bare-*` equivalents to ease porting. Can I use npm modules with Bare? [#can-i-use-npm-modules-with-bare] Pure-JavaScript packages from npm generally work, and Bare uses the npm dependency model. Packages that depend on Node's built-in modules need the corresponding [`bare-*` module](/reference/modules/bare-modules) (or the `bare-node` shim), and packages with native addons need to be built against Bare's addon API rather than Node's N-API (the [`bare-compat-napi`](https://github.com/holepunchto/bare-compat-napi) headers ease that transition). For the full builtin-to-`bare-*` mapping, see [Node.js compatibility](/reference/modules/bare-modules#nodejs-compatibility). Do users need Node.js installed to run a Bare app? [#do-users-need-nodejs-installed-to-run-a-bare-app] No. A Bare program can be compiled into a single standalone executable with `bare-build`—this is how the [`hello-pear-bare`](/getting-started/from-a-template/start-from-hello-pear-bare) template ships—so no Node.js, Bare, or [Pear CLI](/reference/pear/cli) is required on the user's machine. Which JavaScript engine does Bare use? [#which-javascript-engine-does-bare-use] It depends on the build. Bare talks to the engine through the `libjs` ABI, so it can be compiled against V8 (the default), QuickJS via `libqjs`, or JavaScriptCore via `libjsc`. Your application and addons don't change between engines. See also [#see-also] * [Using Bare on its own](/explanation/use-bare-standalone)—the runtime-only path, for adopting Bare without Pear. * [`Bare` runtime reference](/reference/bare/runtime)—the `Bare` global API, lifecycle events, addons, and threads. * [`bare` CLI reference](/reference/bare/cli)—running scripts and the REPL from the command line. * [Bare modules](/reference/modules/bare-modules)—the `bare-*` standard library you opt into. * [One core, many platforms](/explanation/bare-on-native)—embedding the runtime in native and mobile apps. * [Handle app suspension](/how-to/run-on-native/handle-app-suspension)—stop all I/O on `suspend` or the OS will force-terminate the app. * [Runtime and languages](/explanation/runtime-and-languages)—why Pear is JavaScript and how other languages plug in. * [How Pear and Bare fit together](/explanation/pear-and-bare)—where the runtime sits relative to Pear. # Dependencies and network Two questions come up enough during onboarding: * [Why Pear uses npm at all when applications run on Bare, not Node?](#why-npm-for-dependencies) * [What peers can learn about you when you join a swarm?](#what-peers-learn-from-your-ip) Both answers have the same shape—a deliberate trade-off between developer ergonomics and the platform's clean-room ideals. Why npm for dependencies [#why-npm-for-dependencies] npm is a great package manager for JavaScript, and most JavaScript developers already know how to use it. All of Holepunch's modules—[Hypercore](/reference/building-blocks/hypercore), [Hyperdrive](/reference/building-blocks/hyperdrive), [Hyperbee](/reference/building-blocks/hyperbee), the lot—are published there. Reinventing a peer-to-peer package manager would have meant reinventing every developer's habits at the same time, for very little upside. The bootstrap relationship is what's interesting: npm and Node.js are required to **install** Pear initially (`npm install -g pear`), but once the platform is set up neither is needed at runtime. The `pear` command after install is using Bare, not Node, to run your application; npm was just the delivery mechanism. What dependencies your application declares stays meaningful, though. When you `pear stage`, every dependency in your `package.json` is bundled into the application's hyperdrive and replicated to peers along with your code. A few practical implications follow: * **Audit before you ship.** Run `pear stage --dry-run` to review the file list before announcing a release; this is where you'd catch a dependency that ballooned in size or pulled in something unexpected. * **Updates ship fully replicated.** If you update a dependency, you're not merely changing a `package-lock.json`—you're shipping the actual new code to every running peer. Ordinary semver discipline applies, but the consequence is more direct than on a server-rendered web app. Dependency layout for Pear apps [#dependency-layout-for-pear-apps] Bare loads modules from `node_modules` the same way Node does during development, and Pear replicates those trees into the application hyperdrive at stage time. A few layout rules keep staging predictable: 1. **Use `node_modules` for dependencies**—Pear picks them up during development and includes them when you stage. 2. **Use `package.json`**—Pear reads it for application metadata and dependency lists. 3. **Do not bundle dependencies into a single file**—bundlers that inline `node_modules` fight Pear's model of replicating discrete packages. Ship source and dependencies as separate files instead. TypeScript is supported. Not every Holepunch module ships its own typings yet; the community [holepunch-types](https://github.com/Drache93/holepunch-types) project aggregates coverage. An IDE with TypeScript language service helps even for plain JavaScript projects. If you compile TypeScript locally, keep dependencies external. With Bun: ```bash bun build index.ts --packages=external --outdir=. ``` The `--packages=external` flag compiles your application code without inlining `node_modules`. Avoid bundling dependencies into the output artifact you stage. **Never load JavaScript over HTTP(S).** Loading code from an external source is dangerous—if that source is compromised or malicious, your app can be exploited, and the risk is worse for apps with native API access. Pear blocks HTTP and HTTPS code loading by default to prevent this supply-chain risk. Ship dependencies with the app (as above) instead of fetching them at runtime. What peers learn from your IP [#what-peers-learn-from-your-ip] When you connect to a swarm—directly via `hyperswarm`, or transitively because you're running a Pear application that swarms—your IP address is exposed to the peers you connect to. This is unavoidable for the same reason it's unavoidable for any peer-to-peer network: peers need a routable address to reach you, and IP is what TCP/UDP transport provides. This means a peer you connect to can, in principle: * Geolocate you to the level of granularity their IP database supports (typically city, sometimes ISP). * Correlate your presence on multiple swarms if they participate on more than one. * Log timing of your connections and disconnections. What they can't do without further ado: * Read your traffic (Pear connections are end-to-end encrypted via [Secretstream](/reference/helpers/secretstream)) * Impersonate you (your peer key is a public key you control) * Identify you across IP changes (the IP changes; your peer key is what's stable) If your IP is sensitive—you're a journalist, an activist, or just privacy-conscious—route Pear's traffic through a VPN or Tor. The peer-to-peer protocol doesn't care what's underneath the transport, and using a privacy network shifts the IP-disclosure surface from your real address to the exit node's. See also [#see-also] * [Connect to many peers by topic with Hyperswarm](/how-to/connect-to-peers/connect-to-many-peers-by-topic-with-hyperswarm)—joining a swarm in practice. * [Peer-to-peer, demystified](/explanation/peer-to-peer-demystified)—conceptual overview of HyperDHT and Hyperswarm. * [Secretstream](/reference/helpers/secretstream)—the encryption layer on every Pear peer connection. * [Storage and distribution](/explanation/storage-and-distribution)—what actually crosses the wire when peers replicate. * [Hyperswarm reference](/reference/building-blocks/hyperswarm)—topic-based peer discovery and connection management. * [HyperDHT reference](/reference/building-blocks/hyperdht)—the lower-level DHT layer handling hole punching and IP routing. # Deployment - Releasing Apps P2P This page explains **why** many Pear **desktop** release flows use three operations: **stage**, **provision**, and **multisig**, and how **release lines** and the **deployment directory** fit around them. Read it before you run commands so the mental model matches the CLI (concrete steps live under [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment) and [Build desktop distributables](/how-to/operate-an-app/build-and-package/build-desktop-distributables)). {/* Shared snippet (content/_snippets/) included via Fumadocs by the full-control release pages, pointing readers at the CI shortcut for the simple publish/update path. See: https://www.fumadocs.dev/docs/markdown#include */} For the simplest publish/update workflow, let CI stage for you. [Publish with GitHub Actions](/how-to/operate-an-app/github-actions/publish-with-github-actions) turns shipping a new version into a `git push`. This page is the full-control path when you need release lines, provision, and multisig. **Operator detail:** [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment), [Build desktop distributables](/how-to/operate-an-app/build-and-package/build-desktop-distributables), and [Troubleshoot desktop releases](/how-to/operate-an-app/manual-deployment/troubleshoot-desktop-releases) carry the same pipeline as these diagrams. The [Glossary](#glossary) below defines the terms. The short version [#the-short-version] Centralized deploy pipelines often separate *staging servers*, *preview*, and *production*. Pear can fold similar **trust boundaries** into **different Hyperdrives addressed by different `pear://` links**: 1. You **stage** a folder of builds into a drive used for iteration, 2. You **provision** from a versioned stage into a leaner prerelease drive, and 3. You **multisig-commit** into production so a **quorum of signers** must agree. **Each hop can shrink history or raise assurance.** OTA update event lifecycle [#ota-update-event-lifecycle] A running Pear desktop app polls the [Hyperdrive](/reference/building-blocks/hyperdrive) behind its `upgrade` link, downloads new application data when the [Hypercore](/reference/building-blocks/hypercore) length advances, and emits two events from [`pear.updater`](/reference/pear/runtime). The host process forwards them to renderers so the UI can react: * [`updating`](/reference/pear/runtime#updates) fires when the updater begins streaming new blocks; treat it as "background work in progress." * [`updated`](/reference/pear/runtime#updates) fires when the new build is fully on disk; it is safe to swap. * [`bridge.applyUpdate()`](https://github.com/holepunchto/pear-docs/blob/preview/examples/getting-started/pear-chat/electron/preload.js#L9) (renderer side) calls [`pear.updater.applyUpdate()`](https://github.com/holepunchto/pear-runtime-updater#await-updaterapplyupdate) (updater worker side). [`applyUpdate()`](https://github.com/holepunchto/pear-runtime-updater#await-updaterapplyupdate) renames the application directory to the new build and deletes the old one—until the process restarts, the running code is still the old build. * [`bridge.appAfterUpdate()`](https://github.com/holepunchto/pear-docs/blob/preview/examples/getting-started/pear-chat/electron/preload.js#L10) triggers [`app.relaunch()`](https://www.electronjs.org/docs/latest/api/app#apprelaunchoptions) and [`app.quit()`](https://www.electronjs.org/docs/latest/api/app#appquit). On Linux AppImage you usually relaunch with `process.env.APPIMAGE` instead of `process.execPath` so the host wrapper script gets re-executed—see [`app:afterUpdate` in pear-chat](https://github.com/holepunchto/pear-docs/blob/preview/examples/getting-started/pear-chat/electron/main.js#L234). You can disable updates per run with `--no-updates` (handy in development) or globally by setting `"updates": false` in `package.json` and spreading the package config into the `PearRuntime` options. The two-event split is what lets you build UI like the "Update ready" banner in [Reshape into a production app](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app): show a spinner on `updating`, swap to a restart button on `updated`, restart the app on click. Stage, provision, multisig [#stage-provision-multisig] * **Stage**—Local and team checks: feature branches, unsigned or lightly signed builds, ephemeral links. Appends to the application drive; good for iteration. * **Provision**—Prerelease / QA / dogfood: synchronizes from a **versioned** stage source onto a target link while **compacting** additions and deletions so the drive is closer to what stakeholders mirror. * **Multisig**—Production: writes require **co-signing** up to a configured quorum so one compromised machine cannot redefine the release line. Each stage link can feed the next operation; a **provisioned** link becomes the source for **multisig** commits once signers agree. Deployment directory and release lines [#deployment-directory-and-release-lines] A **deployment directory** is the multi-architecture output you assemble after per-OS **make** steps (often with `pear build` merging `darwin`, `linux`, and `win32` artifacts). That directory is what **`pear stage`** reads from disk and writes into [Hypercore](/reference/building-blocks/hypercore)-backed storage for a given **release line** link. **Release lines** are parallel stability tracks: common names are **development**, **staging**, **rc** (each a **staged** link), then **prerelease** (provisioned from `rc`), then **production** (multisig’d from prerelease). The **`upgrade`** field in `package.json` decides **which link** a shipped binary follows for OTA—so different builds can track different lines intentionally. A common pattern is that **`rc`’s `upgrade` pins the production multisig key**, so `rc` builds **do not** receive casual OTA bumps—you ship new installers when that line moves. Release cycle (steady state) [#release-cycle-steady-state] Once bootstrapping finishes, the steady **release cycle** is repetitive: bump **`version`**, **make** per platform, **build** the deployment directory, **stage**, iterate; when stable, **provision**; when assessed, run **multisig** prepare → sign → verify → commit; production goes live; the next cycle starts again at **version**. Numbered labels align with common documentation ordering (touch/seed and upgrade-link setup happen **before** this loop is warm). Foundational steps (bootstrap + loop) [#foundational-steps-bootstrap--loop] The **foundational** diagram adds multisig **key creation** and config **beside** the main loop: signing keys, the multisig config in `pear.json`, pointing `upgrade` at the multisig link, then joining the same release flow. **Stage-only** is enough for proofs of concept; **production** benefits from quorum signing and machine-independent drives. From per-OS builds to one directory [#from-per-os-builds-to-one-directory] That directory must at least contain `package.json` and `by-arch/.../app` trees before `pear stage` is meaningful. Multisig setup and signing [#multisig-setup-and-signing] The diagrams below follow [`hello-pear-electron`'s Foundational Steps](https://github.com/holepunchto/hello-pear-electron#foundational-steps)—the same Electron + Bare worker template [Keet](https://keet.io) and [PearPass](https://pass.pears.com) ship. **One-time** multisig wiring is in [Set up multisig](/how-to/operate-an-app/multisig/set-up-multisig); every production shipment after that uses [Sign with multisig](/how-to/operate-an-app/multisig/sign-with-multisig). If a commit fails mid-flight, see [Troubleshoot multisig](/how-to/operate-an-app/multisig/troubleshoot-multisig). How setup connects to the release cycle [#how-setup-connects-to-the-release-cycle] The [multisig config](/how-to/operate-an-app/multisig/set-up-multisig#create-the-multisig-config) in `pear.json` derives the **multisig link** from its `namespace`, `publicKeys`, and `quorum` alone—it does **not** reference the provision drive. The provision link from the most recent [`hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron) `pear stage` / [`pear provision`](/how-to/operate-an-app/manual-deployment/deployment#6-provision) is instead supplied as the **source** when you prepare and commit each signing request. Shipped binaries read the **multisig link** from [`package.json` `upgrade`](/how-to/operate-an-app/multisig/set-up-multisig#set-upgrade-to-the-multisig-link). Quorum signing [#quorum-signing] Each production commit runs the four steps in [Sign with multisig](/how-to/operate-an-app/multisig/sign-with-multisig): [`pear multisig request`](/how-to/operate-an-app/multisig/sign-with-multisig#prepare-the-request), [`pear multisig sign`](/how-to/operate-an-app/multisig/sign-with-multisig#sign) from each signer until [quorum](/how-to/operate-an-app/multisig/set-up-multisig#create-the-multisig-config) is met, [`pear multisig verify`](/how-to/operate-an-app/multisig/sign-with-multisig#verify), then [`pear multisig commit`](/how-to/operate-an-app/multisig/sign-with-multisig#commit). Release lines [#release-lines] For [`hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron), one [`pear build`](https://github.com/holepunchto/hello-pear-electron#deploying) output can feed parallel **stage** links; **prerelease** and **production** are the provision and multisig gates on the trust ladder. **Custom** lines are optional forks for spikes, hotfixes, or instrumented builds—same mechanics, different seeded link. Relationship to Pear’s global storage story [#relationship-to-pears-global-storage-story] Platform installs still use Pear’s OS-wide tree described in [Storage and distribution](/explanation/storage-and-distribution). The diagrams here are about **application release artifacts** moving between links—not replacing that global layout. Glossary [#glossary] Terms you will see across the **Pear desktop release** and **OTA update** docs: | Term | Meaning | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **OTA** | Over-the-air: delivering a new app build without a traditional reinstall flow from a store. | | **OTA updates** | Syncing the running app’s bundle from a peer-to-peer source when the [application drive](#deployment-directory-and-release-lines) changes. | | **P2P** | Peer-to-peer: machines replicate directly; distribution does not require a single central server if seeders exist. | | **Application drive** | The [Hyperdrive](/reference/building-blocks/hyperdrive) that holds the published app files and version history for a given `pear://` link. | | **Deployment directory** | The multi-architecture folder produced after per-OS builds are assembled (for example with `pear build`) and before `pear stage`. See [Deployment directory and release lines](#deployment-directory-and-release-lines). | | **Multisig** | Co-signing: a quorum of signers must approve before writes land on the production drive. | | **Pear link** | Stable `pear://…` identifier the swarm routes on; see the **Links** row in [Command Line Interface](/reference/pear/cli) and [Storage and distribution](/explanation/storage-and-distribution). | | **Quorum** | Minimum number of multisig signers required to commit a release. | | **Release line** | A parallel deployment stream (for example development, staging, rc) at its own stability level, each with its own staged link. | | **Seeding** | Keeping a drive announced and available so peers can discover and replicate it (`pear seed`). | | **Vendor signing** | OS-level code signing (Apple notarization, Windows Authenticode, etc.) so distributables run on other machines without quarantine. | | **Versioned link** | A `pear://` link that pins `fork`, `length`, and `key` of the [Hypercore](/reference/building-blocks/hypercore) behind the application Hyperdrive—used when provisioning between known versions. | Where to go next [#where-to-go-next] * [Pear desktop application architecture](/explanation/pear-desktop-architecture)—workers, storage, and update events in the running app. * [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment)—operator checklist for `pear stage` / `pear provision` / multisig tooling. * [Build desktop distributables](/how-to/operate-an-app/build-and-package/build-desktop-distributables)—per-OS makes and signing pointers. * [Desktop release npm scripts](/reference/ci-and-release/desktop-release-npm-scripts)—common `package.json` scripts. * [Troubleshoot desktop releases](/how-to/operate-an-app/manual-deployment/troubleshoot-desktop-releases)—OTA, staging, and `pear build` pitfalls. # From append-only logs to files Pear storage is built from a small set of composable primitives stacked on top of one another: At the bottom is **[Hypercore](#hypercore-append-only-blocks)**, an append-only log of verified blocks. Everything else in Pear storage ultimately replicates through one or more cores. **[Hyperblobs](#hyperblobs-large-opaque-objects)** sits on a core when you need opaque objects larger than a single block—it handles chunking and returns stable ids you can store elsewhere. **[Hyperdrive](#hyperdrive-paths-metadata-and-directories)** is the top layer: path-addressed files and directories, with metadata in a [Hyperbee](/reference/building-blocks/hyperbee) tree and file contents in **[Hyperblobs](#hyperblobs-large-opaque-objects)**. Understanding that ladder helps you pick the right building block before you reach for a full filesystem API. This page is conceptual. For API details, see [Hypercore](/reference/building-blocks/hypercore), [Hyperdrive](/reference/building-blocks/hyperdrive), and [Hyperbee](/reference/building-blocks/hyperbee). Hypercore: append-only blocks [#hypercore-append-only-blocks] [Hypercore](/reference/building-blocks/hypercore) is a secure, sparse-replicated append-only log. Each `append` adds one block; readers verify integrity and optionally decrypt per-block. Hypercore is the foundation for nearly every other Pear data structure. You can store arbitrary bytes directly in a core: ```js import Hypercore from 'hypercore' const core = new Hypercore('./my-core') await core.ready() await core.append(Buffer.from('I am a block of data')) ``` That works for small payloads, but Hypercore blocks have a practical size limit. Storing a large file as one block is inefficient; splitting it manually means tracking chunk order, offsets, and retrieval yourself. Hyperblobs: large opaque objects [#hyperblobs-large-opaque-objects] [Hyperblobs](https://github.com/holepunchto/hyperblobs) chunks large data across [Hypercore](/reference/building-blocks/hypercore) blocks and exposes a simple put/get API. A `put` returns an **id** (block and byte offsets and lengths) that you store elsewhere—for example in a [Hyperbee](/reference/building-blocks/hyperbee) key. ```js import Hypercore from 'hypercore' import Hyperblobs from 'hyperblobs' const core = new Hypercore('./blob-core') const blobs = new Hyperblobs(core) await blobs.ready() const id = await blobs.put(Buffer.from('hello world', 'utf-8')) const data = await blobs.get(id) ``` Hyperblobs is the right choice when you need **object storage**—attachments, media, serialized blobs—without file paths or directory semantics. Replication still flows through the underlying Hypercore; peers fetch missing blob blocks like any other core data. [Hyperdrive](/reference/building-blocks/hyperdrive) uses Hyperblobs internally for file contents. You do not need Hyperdrive if all you want is keyed blob storage. Hyperdrive: paths, metadata, and directories [#hyperdrive-paths-metadata-and-directories] [Hyperdrive](/reference/building-blocks/hyperdrive) combines a [Hyperbee](/reference/building-blocks/hyperbee) metadata tree with a Hyperblobs content store. You get path-based operations—`get`, `put`, `del`, `list`, existence checks—similar to a filesystem, with the same replication properties as [Hypercore](/reference/building-blocks/hypercore). ```js import Hyperdrive from 'hyperdrive' import Corestore from 'corestore' const store = new Corestore('./drive-storage') const drive = new Hyperdrive(store) await drive.ready() await drive.put('/notes/readme.txt', Buffer.from('hello')) const buffer = await drive.get('/notes/readme.txt') ``` Pear application bundles are Hyperdrives: code, assets, and dependencies staged with `pear stage` live in a drive that peers replicate. User-facing apps often keep **application state** in separate cores or drives under [Corestore](/reference/helpers/corestore). Helpers extend the model without changing the stack: * [Localdrive](/reference/helpers/localdrive)—mirror between a Hyperdrive and a local folder. * [Mirrordrive](/reference/helpers/mirrordrive)—copy or sync between two drives. Choosing a layer [#choosing-a-layer] | Need | Use | | --------------------------------- | --------------------------------------------------------------------------------------- | | Ordered event log, small messages | [Hypercore](/reference/building-blocks/hypercore) directly | | Large binary object, no path | Hyperblobs on a dedicated core | | Files, directories, bundle layout | [Hyperdrive](/reference/building-blocks/hyperdrive) | | Sorted key/value over one log | [Hyperbee](/reference/building-blocks/hyperbee) (often inside Hyperdrive or standalone) | If you do not need path-addressed files shared between peers, prefer Hyperblobs over Hyperdrive. Hyperdrive pays for directory semantics and metadata indexing you may not use. See also [#see-also] * [Storage and distribution](/explanation/storage-and-distribution)—where cores and drives live on disk in a Pear installation. * [Share append-only databases with Hyperbee](/how-to/store-and-replicate/share-append-only-databases-with-hyperbee)—key/value patterns on Hypercore. * [Work with many Hypercores using Corestore](/how-to/store-and-replicate/work-with-many-hypercores-using-corestore)—managing multiple cores from one storage root. * [Hypercore reference](/reference/building-blocks/hypercore)—append-only log API. * [Hyperdrive reference](/reference/building-blocks/hyperdrive)—filesystem API. * [Hyperbee reference](/reference/building-blocks/hyperbee)—sorted key/value tree. # About Pear The pages here are for understanding, not doing. They answer "why" rather than "how": why does Pear use append-only logs, what does "peer-to-peer" mean in practice, what's the difference between Bare and Node, and why is there no central server. If you want to build something, head to **[How To](/how-to)**. If you need to look up a specific API, that's **[Reference](/reference)**. In this section [#in-this-section] Pages are grouped by topic and ordered roughly from foundational concepts to app-specific ones. Platform foundations [#platform-foundations] * [How Pear and Bare fit together](/explanation/pear-and-bare)—how Pear and Bare layer together, from the native C foundations up to the apps you run. * [Peer-to-peer, demystified](/explanation/peer-to-peer-demystified)—hole punching, public-key identity, and the roles of [HyperDHT](/reference/building-blocks/hyperdht) and [Hyperswarm](/reference/building-blocks/hyperswarm). * [Runtime and languages](/explanation/runtime-and-languages)—JavaScript on Bare, native addons for other languages, and the Pear-end pattern for cross-platform apps. * [Using Bare on its own](/explanation/use-bare-standalone)—the entry point for adopting Bare as a standalone runtime, without Pear's peer-to-peer platform. * [Inside Bare](/explanation/bare-runtime)—what the Bare runtime adds, its lifecycle, and why its standard library is opt-in. * [One core, many platforms](/explanation/bare-on-native)—running the peer-to-peer core in a Bare worklet behind a typed RPC seam. * [Dependencies and network](/explanation/dependencies-and-network)—why NPM is the install path, the runtime relationship to Node, and what your IP discloses on a swarm. Storing & replicating data [#storing--replicating-data] * [From append-only logs to files](/explanation/from-logs-to-files)—Hypercore, Hyperblobs, and [Hyperdrive](/reference/building-blocks/hyperdrive)—which storage layer to use and why. * [Storage and distribution](/explanation/storage-and-distribution)—where Pear keeps your data, what the swarm replicates, and how new releases reach users. * [Availability and blind peering](/explanation/availability-and-blind-peering)—seeding, its limits, and always-on replication without reading user data. Building & shipping apps [#building--shipping-apps] * [Pear desktop application architecture](/explanation/pear-desktop-architecture)—how a typical Pear Electron app splits updates, storage, and Bare workers (with a link to a full sample repo). * [Workers](/explanation/workers)—why peer-to-peer logic lives in a Bare worker behind one IPC stream, and where the host/worker boundary should sit. * [Release pipeline](/explanation/deployment-releasing-apps-p2p)—stage, provision, multisig, release lines, and deployment-directory diagrams (agnostic; sample implementation on GitHub). {/* Planned explanation pages. Uncomment each bullet as the page lands. More conceptual overviews are in flight; topics planned to land here include: - **What is Pear?**—the runtime, the toolchain, and the philosophy in plain language. - **Append-only logs are a database**—why [Hypercore](/reference/building-blocks/hypercore) looks weird if you're coming from SQL. - **The runtime model**—how Pear apps start, update, and shut down. */} Coming from the old FAQ? [#coming-from-the-old-faq] The single FAQ page was retired. Each question now lives in the section it actually belongs to: | Old FAQ question | New home | | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | *How do I get a list of installed applications?* | [Manage installed applications](/how-to/manage-installed-applications) | | *How do I uninstall a Pear application?* | [Manage installed applications](/how-to/manage-installed-applications) | | *Where is the Pear application stored?* | [Storage and distribution](/explanation/storage-and-distribution) | | *Can Pear be used with X language?* | [Runtime and languages](/explanation/runtime-and-languages) | | *How do I write an application once that runs on desktop, mobile, etc.?* | [Runtime and languages](/explanation/runtime-and-languages) · [One core, many platforms](/explanation/bare-on-native) | | *Is Bare a fork of Node? Can I use npm modules?* | [Inside Bare](/explanation/bare-runtime) | | *Can Bare run on iOS or Android?* | [One core, many platforms](/explanation/bare-on-native) | | *How is my application distributed? Do I have to keep `pear seed` running?* | [Storage and distribution](/explanation/storage-and-distribution) | | *Why is NPM used for dependencies?* | [Dependencies and network](/explanation/dependencies-and-network) | | *How do I distribute a binary version of my application?* | [Distribute as a binary](/how-to/operate-an-app/build-and-package/distribute-as-binary) | | *Can peers know my IP address when using `hyperswarm`?* | [Dependencies and network](/explanation/dependencies-and-network) | {/* Writing a new explanation? See [Diátaxis on explanation](https://diataxis.fr/explanation/)—discursive, comparative, opinionated where it helps. Don't list APIs; that's reference. Don't tell readers to do anything; that's how-to. */} # How Pear and Bare fit together Pear is the platform people see: a runtime, a CLI, and over-the-air updates for peer-to-peer apps. [Bare](/explanation/bare-runtime) is the engine underneath it. They sit at different layers, and knowing which layer you're working at makes the rest of these docs easier to navigate. This page lays out the layers, bottom to top, and shows where each library you'll meet elsewhere belongs. The layers at a glance [#the-layers-at-a-glance] Each layer runs on the one beneath it. Read from the bottom up, it's a story about how raw C bindings become a peer-to-peer app you can install. The engine room: native foundations and the Bare runtime [#the-engine-room-native-foundations-and-the-bare-runtime] At the very bottom are C libraries: [`libjs`](https://github.com/holepunchto/libjs) (and its JavaScriptCore-backed sibling [`libjsc`](https://github.com/holepunchto/libjsc)) give an engine-independent, ABI-stable way to talk to a JavaScript engine; [`libuv`](https://github.com/libuv/libuv) provides the asynchronous I/O event loop; [`libudx`](https://github.com/holepunchto/libudx) carries the reliable UDP streams the networking stack rides on. [Bare](/explanation/bare-runtime) sits directly on top. It's a small, embeddable JavaScript runtime that adds only what those C primitives can't supply on their own: a module system (with CommonJS/ESM interop), a native-addon loader, and lightweight threads. Everything else is left to userland—which is the next layer up. Bare userland: the bare-* modules [#bare-userland-the-bare--modules] Bare ships almost no standard library. The familiar runtime surface—file system, sockets, crypto, HTTP—lives in installable [`bare-*` modules](/reference/modules/bare-modules) you opt into. The same layer holds two other families that matter for cross-platform apps: * **Embedding & native** ([`bare-kit`](/reference/bare/bare-kit), [`react-native-bare-kit`](/reference/bare/bare-kit), and the typed RPC seam of [`bare-rpc`](/reference/modules/bare-modules) + [`hyperschema`](https://github.com/holepunchto/hyperschema)) let a native app run a Bare core on a background thread. See [One core, many platforms](/explanation/bare-on-native). * **Build & bundle** (`bare-pack`, `bare-bundle`, `bare-make`, `bare-build`) turn a Bare program and its dependencies into an embeddable bundle or a standalone executable. The peer-to-peer building blocks [#the-peer-to-peer-building-blocks] The libraries Pear is famous for—[Hypercore](/reference/building-blocks/hypercore), [Hyperbee](/reference/building-blocks/hyperbee), [Hyperdrive](/reference/building-blocks/hyperdrive), [Autobase](/reference/building-blocks/autobase), [Hyperswarm](/reference/building-blocks/hyperswarm), [HyperDHT](/reference/building-blocks/hyperdht), and [Corestore](/reference/helpers/corestore)—are ordinary JavaScript modules. They have no special status in the runtime; they run *on* Bare like any other dependency. That's why the same storage and networking code works unchanged whether it's hosted by Electron on the desktop or a worklet on a phone. The Pear platform [#the-pear-platform] [Pear](/) is the layer that turns those building blocks into a product: [`pear-runtime`](/reference/pear/runtime) spawns and updates your app, the [`pear-*` modules](/reference/modules/pear-modules) supply application services, and the [Pear CLI](/reference/pear/cli) stages, seeds, and releases it. Pear is built *on* Bare and the building blocks—it doesn't replace them. Apps: one core, many UI hosts [#apps-one-core-many-ui-hosts] At the top are the apps—[Keet](https://keet.io), [PearPass](https://pass.pears.com), and whatever you build. The recommended shape splits an app into a portable JavaScript **core** (peer-to-peer logic and storage) and a thin **UI host** that changes per platform: Electron on desktop, a native shell on mobile, a standalone binary in the terminal. The core is the same code everywhere; only the host swaps. That split is the subject of [Runtime and languages](/explanation/runtime-and-languages) and [One core, many platforms](/explanation/bare-on-native). See also [#see-also] * [Inside Bare](/explanation/bare-runtime)—what the runtime layer actually adds. * [One core, many platforms](/explanation/bare-on-native)—how the portable core reaches mobile and native apps. * [Runtime and languages](/explanation/runtime-and-languages)—why the stack is JavaScript and how other languages join in. * [Bare modules](/reference/modules/bare-modules)—the catalog of `bare-*` userland modules. * [Pear modules](/reference/modules/pear-modules)—the `pear-*` libraries at the platform layer. # Pear desktop application architecture This page explains **why** a production-style Pear **Electron** app usually separates three concerns: 1. **over-the-air (OTA) updates**, 2. **persistent peer-to-peer storage**, and 3. **Bare workers** behind a single IPC stream. Read it if you are sketching your own main/renderer/worker split or comparing a full runtime setup to the minimal [getting started chat](/getting-started/build-a-peer-to-peer-chat/build-a-peer-to-peer-chat). The short version [#the-short-version] The pattern optimizes to: * **ship new builds without a central server** * **keep replication-capable storage beside the app** * **keep native P2P modules out of the renderer** A [**Bare worker**](/explanation/workers) owns the [`pear-runtime`](/reference/pear/runtime) instance together with [Hyperswarm](/reference/building-blocks/hyperswarm), [Hypercore](/reference/building-blocks/hypercore), and the [OTA updater](/reference/pear/runtime#updates); Electron's [main process](#process-model) is a thin shell that spawns the worker and forwards [IPC](/explanation/workers#the-ipc-contract); the [renderer](#process-model) stays a normal web view. Updates flow when the [**application Hyperdrive**](/reference/building-blocks/hyperdrive) changes; the runtime signals the UI, then swaps paths on disk so the [**next process start**](#updates) runs the new code. The official [`hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron) template uses this worker-hosted shape, and the [getting started production-shape part](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app) follows it—running the `pear-runtime` updater in its own dedicated Bare worker so update traffic never blocks the chat worker. Process model [#process-model] The renderer does not load `hyperswarm`, `hypercore`, or other Bare-oriented modules directly. The main process starts the worker with [`PearRuntime.run`](/reference/pear/runtime#running-workers) (or the instance method [`pear.run`](/reference/pear/runtime#running-workers)) and forwards bytes between the worker’s [`Bare.IPC`](/reference/pear/runtime#running-workers) stream and whatever bridge you expose to the renderer. Updates [#updates] An **update** is triggered when a **seeded application drive** gains new writes: the replicated [Hyperdrive](/reference/building-blocks/hyperdrive) behind your `pear://` link reflects a new staged or provisioned build. The runtime emits **`updating`** while it syncs, then **`updated`** when the new bundle is ready. These events are emitted on the runtime's [`pear.updater`](/reference/pear/runtime#updates) sub-object—handlers attach with `pear.updater.on('updating')` / `pear.updater.on('updated')`, and the staged update is applied with `pear.updater.applyUpdate()`. After `updated`, a common shell **repoints the active application path** to the freshly synced build and removes the old tree from disk so the **next start** runs the new code. That matches OTA expectations: sync in the background, switch on restart (or after your UI explicitly restarts). Disable updates per run (`--no-updates`) or with [`package.json` `updates: false`](/reference/pear/runtime#disabling-updates) during local development so a seeded link does not replace your working tree mid-session. Storage [#storage] The runtime’s **`dir`** option is the root for **peer-to-peer and local application data**. In production that usually maps to per-OS application support directories; in development many teams use a **separate dev default** or a flag. [`pear.storage`](/reference/pear/runtime#storage) is the string you pass into [`Corestore`](/reference/helpers/corestore) so every core shares one coherent on-disk layout. Passing **`--storage /path`** (or equivalent) spins up an **isolated Corestore**—the same idea as running two app instances on two machines: different storage roots, no key collisions. Workers [#workers] **Application P2P logic**—swarms, cores, drives—belongs in the worker entrypoint. Arguments you pass from `pear.run('./workers/main.js', [pear.storage, …])` show up as [`Bare.argv`](/reference/pear/api#bare-argv) inside the worker (`argv[0]`/`argv[1]` are reserved; **`argv[2]`** is the first user argument, which is why sample apps often pass storage at index `2`). Rule of thumb: **main = shell + IPC**, **worker = data plane**, **renderer = view**. Where this differs from the minimal tutorial [#where-this-differs-from-the-minimal-tutorial] The [getting started chat](/getting-started/build-a-peer-to-peer-chat/build-a-peer-to-peer-chat) walkthrough uses [`PearRuntime.run()`](/reference/pear/runtime#running-workers) so you can paste a tiny worker without configuring `upgrade` or `dir`. A shipped desktop app switches on the **full `new PearRuntime({ ... })` constructor** so OTA, storage, and the updater share one runtime object—the shape in the [`pear-runtime` reference](/reference/pear/runtime). In the official [`hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron) template, that constructor runs inside the Bare worker, keeping the main process a thin proxy; the [getting started production-shape part](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app) does the same, running the updater in its own dedicated Bare worker. Where to go next [#where-to-go-next] * [Start from the hello-pear-electron template](/getting-started/from-a-template/start-from-hello-pear-electron)—a hands-on tour of this architecture in the official boilerplate. * [Release pipeline](/explanation/deployment-releasing-apps-p2p)—stage, provision, multisig, and diagrams for moving builds between links. * [Release pipeline glossary](/explanation/deployment-releasing-apps-p2p#glossary)—terminology in one table. * [Storage and distribution](/explanation/storage-and-distribution)—Pear-wide layout versus per-app `dir`. * [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment)—when you are ready to run commands. **Related documentation** **Getting Started:** [Part 1: Chat](/getting-started/build-a-peer-to-peer-chat/build-a-peer-to-peer-chat) · [Part 2: Production-shaped](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app) · [Part 3: Ship](/getting-started/build-a-peer-to-peer-chat/ship) · [Part 4: Update](/getting-started/build-a-peer-to-peer-chat/update) **Operating an app** * [Build desktop distributables](/how-to/operate-an-app/build-and-package/build-desktop-distributables) * [Desktop release npm scripts](/reference/ci-and-release/desktop-release-npm-scripts) * [Distribute as a binary](/how-to/operate-an-app/build-and-package/distribute-as-binary) * [Manage installed applications](/how-to/manage-installed-applications) * [Troubleshoot desktop releases](/how-to/operate-an-app/manual-deployment/troubleshoot-desktop-releases) * [Troubleshoot common issues](/how-to/troubleshooting) # Peer-to-peer, demystified import { Cards, Card } from 'fumadocs-ui/components/card' Pear applications replicate data and connect to one another without a central server. Peers exchange bytes directly over encrypted streams; there is no single host that routes every message or stores every copy. That model changes how discovery, connectivity, and privacy work compared with a traditional client–server application—but it does not remove the need for careful application design. This page explains networking concepts. For step-by-step connection guides, see [Connect two peers by key with HyperDHT](/how-to/connect-to-peers/connect-two-peers-by-key-with-hyperdht) and [Connect to many peers by topic with Hyperswarm](/how-to/connect-to-peers/connect-to-many-peers-by-topic-with-hyperswarm). What changes without a server [#what-changes-without-a-server] In a client–server design, clients know where to connect: a hostname or IP address, often fronted by a load balancer. In Pear, **peers are identified by cryptographic public keys**, not by fixed network locations. A peer can move between Wi‑Fi networks, cellular connections, or countries and still be reachable at the same key. Replication and chat still need **at least one peer online** with a copy of the data you want—Pear does not magically materialize files from nowhere. What changes is *who* can serve that copy: any participant in the swarm, not only the original author. What does **not** change: you still need to think about availability, authorization, and what metadata leaks at the transport layer. * [Storage and distribution](/explanation/storage-and-distribution)—how application bundles and user data reach peers over the same primitives. * [Dependencies and network](/explanation/dependencies-and-network)—what peers can infer from your IP when you join a swarm. Hole punching and NAT traversal [#hole-punching-and-nat-traversal] Most devices sit behind home routers or carrier-grade network address translation (NAT). Two peers cannot open a Transmission Control Protocol (TCP) connection to each other simply by knowing each other's keys—neither side has a stable, publicly routable address that the other can dial first. **User Datagram Protocol (UDP) hole punching** coordinates both peers through a rendezvous point so that each opens a path through its local firewall. Once the "hole" exists, Pear's transport can carry encrypted traffic over it. Hole punching works on most consumer networks; it is not guaranteed on every corporate or symmetric-NAT setup, which is why Pear also supports relay paths when direct connectivity fails. [HyperDHT](#hyperdht-discovery-and-encrypted-connections) implements this discovery and connection machinery. You rarely call hole punching directly; you create a DHT node or join a [Hyperswarm](/reference/building-blocks/hyperswarm) topic and the stack handles traversal. HyperDHT: discovery and encrypted connections [#hyperdht-discovery-and-encrypted-connections] [HyperDHT](/reference/building-blocks/hyperdht) is a distributed hash table. It is the layer that finds peers and establishes **end-to-end encrypted** connections using Noise streams (via [Secretstream](/reference/helpers/secretstream)). Core mechanisms: 1. **Peer identification**—Connections are addressed by public key, not IP address. Location and network changes do not invalidate identity. 2. **Hole punching**—UDP techniques to connect through NATs and firewalls on typical networks. 3. **Encrypted streams**—Every connection is encrypted; intermediaries see traffic volume and endpoints, not payload. 4. **Bootstrapping**—Default bootstrap nodes help a new node join the public DHT; isolated networks can use custom bootstrap lists. 5. **Announcement and discovery**—Peers announce themselves under 32-byte topics; others look up who is listening on a topic. Peers with a known public key can also connect directly without topic lookup. See [Connect two peers by key with HyperDHT](/how-to/connect-to-peers/connect-two-peers-by-key-with-hyperdht) for a walkthrough of how to use HyperDHT. HyperDHT also supports limited mutable and immutable storage in the DHT itself. Most Pear applications instead replicate structured data over [Hypercore](/reference/building-blocks/hypercore) once a connection exists; the DHT's primary job is **finding and connecting**, not long-term storage. Hyperswarm: topic-based connection management [#hyperswarm-topic-based-connection-management] [Hyperswarm](/reference/building-blocks/hyperswarm) wraps [HyperDHT](/reference/building-blocks/hyperdht) with a higher-level API oriented around **topics**—arbitrary 32-byte identifiers, often a [Hypercore](/reference/building-blocks/hypercore)'s `discoveryKey`. Where HyperDHT exposes servers, clients, announce streams, and lookup streams, Hyperswarm exposes `join`, `leave`, and a single `connection` event. [Connect to many peers by topic with Hyperswarm](/how-to/connect-to-peers/connect-to-many-peers-by-topic-with-hyperswarm) for a walkthrough of how to use Hyperswarm. Hyperswarm adds: * **Automatic reconnection** when peers drop off the network. * **Connection limits and firewall hooks** to cap fan-out or reject unwanted keys. * **Client/server roles per topic**—a peer can announce only, discover only, or both. * **Direct peer joins** via `joinPeer(publicKey)` when you already know whom to reach. For replication workflows—chat rooms, file sync, application updates—Hyperswarm is usually the right default: you join the discovery key of the core or drive you care about and let the swarm maintain connections. HyperDHT remains the right choice when you need fine-grained control over server lifecycle, custom DHT storage, or minimal dependencies. Choosing between HyperDHT and Hyperswarm [#choosing-between-hyperdht-and-hyperswarm] | Concern | [HyperDHT](/reference/building-blocks/hyperdht) | [Hyperswarm](/reference/building-blocks/hyperswarm) | | -------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------ | | API surface | Low-level: nodes, servers, sockets | High-level: topics, connections | | Reconnection | Your responsibility | Built in | | Typical use | Custom protocols, direct key connections | [Hypercore](/reference/building-blocks/hypercore) replication, multi-peer apps | | Underlying transport | UDX + hole punching | Same (uses HyperDHT internally) | If you are replicating a Hypercore or [Hyperdrive](/reference/building-blocks/hyperdrive), start with Hyperswarm unless you have a specific reason to manage DHT nodes yourself. See also [#see-also] * [Connect two peers by key with HyperDHT](/how-to/connect-to-peers/connect-two-peers-by-key-with-hyperdht)—minimal two-peer connection walkthrough. * [Connect to many peers by topic with Hyperswarm](/how-to/connect-to-peers/connect-to-many-peers-by-topic-with-hyperswarm)—topic-based discovery and chat. * [Replicate and persist with Hypercore](/how-to/store-and-replicate/replicate-and-persist-with-hypercore)—persistence once peers are connected. * [Dependencies and network](/explanation/dependencies-and-network)—IP disclosure and NPM's role at install time. * [From append-only logs to files](/explanation/from-logs-to-files)—how replicated data is structured above the transport layer. * [HyperDHT reference](/reference/building-blocks/hyperdht)—full API. * [Hyperswarm reference](/reference/building-blocks/hyperswarm)—full API. # Runtime and languages Pear applications are JavaScript programs. The runtime under them is **[Bare](/explanation/bare-runtime)**, a small embeddable JavaScript runtime that strips the assumptions Node.js carries about being a server. This page explains why the language story looks the way it does, how non-JavaScript code joins in, and what the recommended app shape is when you want one codebase to run on desktop, mobile, and terminal. For where Bare sits relative to Pear, see [How Pear and Bare fit together](/explanation/pear-and-bare). Why JavaScript [#why-javascript] Two pragmatic reasons: 1. **Web heritage.** The peer-to-peer libraries Pear is built on ([Hypercore](/reference/building-blocks/hypercore), [Hyperdrive](/reference/building-blocks/hyperdrive), [Hyperswarm](/reference/building-blocks/hyperswarm)) are mature JavaScript modules with millions of downloads. Reusing them on the desktop and on mobile through a single language eliminates a translation layer. 2. **Bare keeps it small.** Bare ships without Node's standard library opinions—no `fs.read` on a URL, no built-in `http` module, no Worker baggage. That makes it cheap to embed in mobile apps, desktop shells, and constrained terminal environments alike. The Pear runtime adds the bits applications actually need on top. If you're coming from Node.js, the surface looks similar but isn't identical—see [Bare modules](/reference/modules/bare-modules) for the exact set of standard libraries available, and the [Node.js compatibility](/reference/modules/bare-modules#nodejs-compatibility) section for a builtin-by-builtin mapping to the corresponding `bare-*` modules. The [`bare-node`](https://github.com/holepunchto/bare-node) shim maps many Node.js builtins onto their Bare equivalents, easing the port. Other languages: native addons [#other-languages-native-addons] For code that JavaScript can't naturally express—tight cryptography loops, codec work, hardware access—Bare loads native addons. The [`bare-addon`](https://github.com/holepunchto/bare-addon) template is the starting point: write the addon in C, C++, Rust, or any language with a C-compatible ABI, expose it through Bare's addon API, and call it from JavaScript like any module. For languages that compile to JavaScript (TypeScript being the obvious one) the path is unchanged: compile to JS, then point the application's [`main` field or HTML `

Pear chat

peers: 0
``` * **Line 7:** Tailwind v4 browser build from jsDelivr. * **Lines 29–31:** peer counter in the header (`#peers`). * **Line 34:** scrollable message log (`#log`). * **Lines 36–41:** text input (`#input`). * **Line 43:** loads `app.js`. 2. Create the app.js file [#2-create-the-appjs-file] Create `renderer/app.js`. It sets up the DOM and listens for events from the worker: * **Lines 29–31:** if the event type is `peers`, update the counter; if `message` or `ready`, append a row. * **Lines 34–39:** when you press Enter, append locally and call `window.chat.send(text)`—which flows through **lines 5–7** of `preload.js` and **lines 39–41** of `electron/main.js` to **line 28** of `workers/main.mjs`. ```js file=/examples/getting-started/chat/renderer/app.js title="renderer/app.js" lineNumbers {28-32,34-39} const log = document.getElementById('log') const peers = document.getElementById('peers') const input = document.getElementById('input') const fromColor = { system: 'text-zinc-500 italic', you: 'text-emerald-400 font-medium', peer: 'text-sky-400 font-medium' } function append(from, text) { const row = document.createElement('div') row.className = 'flex gap-2 items-baseline' const fromEl = document.createElement('span') fromEl.className = fromColor[from] ?? fromColor.peer fromEl.textContent = from + ':' const textEl = document.createElement('span') textEl.textContent = text row.append(fromEl, textEl) log.appendChild(row) log.scrollTop = log.scrollHeight // keep latest line visible } // JSON lines from the worker, forwarded by preload → main → here window.chat.onMessage((event) => { if (event.type === 'peers') peers.textContent = event.count else if (event.type === 'message') append(event.from, event.text) else if (event.type === 'ready') append('system', 'connected to swarm') }) input.addEventListener('keydown', (e) => { if (e.key !== 'Enter' || !input.value) return const text = input.value input.value = '' append('you', text) window.chat.send(text) // preload → main → worker Bare.IPC }) ``` Run it [#run-it] From the `pear-chat` folder run: ```bash npm start ``` A small dark window opens. The header shows `peers: 0` (**line 31** of `index.html`) and the message log is empty. After about 5 seconds your worker finishes joining the DHT and the log shows `system: connected to swarm` (**line 31** of `app.js`, triggered by **line 34** of `workers/main.mjs`). Open a second terminal in the same folder and run `npm start` again. Within a few seconds the two peers find each other and the header on **both** windows ticks to `peers: 1`. Type `hey` in one window and press Enter. The other window shows it prefixed with the sender's short peer id—a six-character hex slice of the remote public key. What is running: * two Electron processes * two [Bare workers](/explanation/runtime-and-languages) * one DHT topic * a direct, end-to-end encrypted peer-to-peer connection between them First connection typically takes 5–15 seconds while [Hyperswarm](/reference/building-blocks/hyperswarm) announces the topic to the DHT. If both peers are on the same machine they connect over the local network; on different networks they hole-punch through the DHT. If two readers happen to share an OS username they find each other on the public DHT—change the prefix string on **line 11** of `electron/main.js` if that bothers you. What you built [#what-you-built] A peer-to-peer desktop chat that runs without a server. Each piece maps to one concept: | File | What it does | Pear concept | | ---------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `package.json` | Declares Electron, [Hyperswarm](/reference/building-blocks/hyperswarm), and `pear-runtime` as deps | What you embed into a JS host | | `workers/main.mjs` | Hyperswarm + topic + connection handlers | Peer-to-peer code lives in a [Bare worker](/explanation/runtime-and-languages) | | `electron/main.js` | `PearRuntime.run()` spawns the worker | The worker is your local backend | | `electron/preload.js` | `contextBridge.exposeInMainWorld('chat', { ... })` | The single door from renderer to worker | | `renderer/*.{html,js}` | Plain DOM, no Pear imports | The view layer is a plain web page | Three things are deliberately missing—[part 2](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app) adds them: * **No persistence.** Messages disappear when you close the window. [Part 2](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app) adds an [Autobase](/reference/building-blocks/autobase)-backed room persisted to disk with [Corestore](/reference/helpers/corestore). * **No over-the-air updates.** [Part 2](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app) wires a dedicated OTA updater worker with [`pear.updater`](/reference/pear/runtime) events. * **No packaging.** `npm start` runs Electron in development mode. [Ship your app](/getting-started/build-a-peer-to-peer-chat/ship) covers `electron-forge`, `pear build`, and the first stage; [Deploy over-the-air updates](/getting-started/build-a-peer-to-peer-chat/update) walks the live OTA loop and the `stage → provision → multisig` production flow. Where to go next [#where-to-go-next] * **Continue the path:** [Reshape into a production app](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app). * For the production-grade Electron starter you would actually clone for a real project, see [`hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron). * To swap [Hyperswarm](/reference/building-blocks/hyperswarm) for a direct one-to-one connection by public key, see [Connect two peers by key with HyperDHT](/how-to/connect-to-peers/connect-two-peers-by-key-with-hyperdht). * To understand why Pear ships with two runtimes (Node-style for Electron, Bare for workers), read [Runtime and languages](/explanation/runtime-and-languages). # Reshape into a production app import { Steps, Step } from 'fumadocs-ui/components/steps' This is **part 2 of 4** in the [getting started path](/getting-started). [Part 1](/getting-started/build-a-peer-to-peer-chat/build-a-peer-to-peer-chat) built a working chat from scratch with the simplest form of the runtime ([`PearRuntime.run`](/reference/pear/runtime#running-workers)). This part reshapes it into a **production-shaped scaffold** built on the same patterns as Pear's official [`hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron) template—a Bare worker behind a preload bridge, an OTA updater worker, and Electron Forge packaging—the same patterns [Keet](https://keet.io) and [PearPass](https://pass.pears.com) use under the hood. By the end you have the shared **`pear-chat` scaffold** every chat-family and media how-to in [the How To guides](/how-to) extends. There is no separate intermediate app—part 2 is the finished template shape: * A separate [Bare worker](/explanation/runtime-and-languages) that owns all the P2P code. * An [Autobase](/reference/building-blocks/autobase)-backed shared room with [blind-pairing](https://www.npmjs.com/package/blind-pairing) invite codes, so two peers join from a short string without exposing keys. * On-disk persistence via [Corestore](/reference/helpers/corestore) + a [HyperDB](https://www.npmjs.com/package/hyperdb) view—the conversation survives restarts. * A separate [OTA updater worker](/explanation/deployment-releasing-apps-p2p#ota-update-event-lifecycle), wired but inert, ready for [part 3](/getting-started/build-a-peer-to-peer-chat/ship) and [part 4](/getting-started/build-a-peer-to-peer-chat/update) to flip on. * A polished [Tailwind CSS](https://tailwindcss.com/) UI (compiled by the CLI, not the CDN) with a peer counter and a **copy-invite** button. {/* Shared snippet (content/_snippets/) included via Fumadocs by every getting-started step. Reminds readers that a production-ready reference implementation already exists upstream, so they can clone it any time without abandoning the type-along. See: https://www.fumadocs.dev/docs/markdown#include */} **Full production-ready reference: `hello-pear-electron`.** The complete version of this chat lives at [`holepunchto/hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron)—Holepunch's official Electron template, the same shape [Keet](https://keet.io) and [PearPass](https://pass.pears.com) ship. Clone it any time to see the finished structure or to crib code. For a guided tour of the template, see [Start from the hello-pear-electron template](/getting-started/from-a-template/start-from-hello-pear-electron). What you'll build [#what-youll-build] A standalone Electron app that: * Runs the actual peer-to-peer code inside a [Bare worker](/explanation/runtime-and-languages), keeping native modules out of the renderer and Electron's main thread: * [Hyperswarm](/reference/building-blocks/hyperswarm) finds and connects peers over the DHT. * [Corestore](/reference/helpers/corestore) is the on-disk store that holds the app's Hypercores. * [Autobase](/reference/building-blocks/autobase) is the multi-writer log that merges every peer's messages into one shared room view. * [blind-pairing](https://www.npmjs.com/package/blind-pairing) lets a new peer join from a short invite code without either side exposing its keys. * Pairs two peers via an **invite code** generated from the room owner's Autobase key—the UI exposes it behind a copy button. * Persists every message to disk in an [Autobase-backed HyperDB view](/reference/building-blocks/autobase), so reopening the app replays the conversation. * Embeds the [`pear-runtime`](/reference/pear/runtime) OTA updater in its **own** Bare worker, so update traffic never blocks the chat. * Renders a vanilla HTML + [Tailwind CSS](https://tailwindcss.com/) UI that talks to the worker by exchanging JSON over a framed pipe—no bundler, no framework. For the conceptual picture behind this split—why a production app separates OTA updates, storage, and workers—read [Pear desktop application architecture](/explanation/pear-desktop-architecture). Before you start [#before-you-start] You need: * [Node.js](https://nodejs.org/) v22.17 or newer and npm v10.9 or newer. * A POSIX-style terminal (macOS, Linux, or Windows with WSL). * An IDE or text editor. * The working chat from [part 1—build the peer-to-peer chat](/getting-started/build-a-peer-to-peer-chat/build-a-peer-to-peer-chat), so you are comfortable with [`PearRuntime.run()`](/reference/pear/runtime#running-workers), [Bare workers](/explanation/workers), and [Hyperswarm](/reference/building-blocks/hyperswarm) topics. Part 1 was type-along in five files. The production scaffold is \~14 files and a generated `spec/` directory—too much to retype. **Clone the repo** and walk the working code as this part explains it. Every snippet points to a real file so you can read the surrounding context. Clone the example [#clone-the-example] ```bash skip="desktop-gui" git clone https://github.com/holepunchto/pear-docs cd pear-docs git switch published cd examples/getting-started/pear-chat npm install npm run build ``` `npm run build` does two things you must run before the first `npm start`: * `build:db` runs `node schema.js` to generate the `spec/` directory (HyperSchema records, the HyperDB view, and the HyperDispatch encoders). * `build:tailwind` compiles `input.css` into `renderer/build/output.css` with the [Tailwind CLI](https://tailwindcss.com/docs/installation)—replacing part 1's in-browser CDN script. You should now have a `pear-chat/` directory laid out like this: ```text pear-chat/ ├─ build/ # icons + per-OS packaging assets ├─ electron/ │ ├─ main.js # spawns the chat worker + the updater worker │ └─ preload.js # contextBridge exposing window.bridge ├─ renderer/ │ ├─ app.js # vanilla DOM + worker bridge │ └─ index.html # chat shell (Tailwind classes) ├─ spec/ # generated HyperDispatch + HyperDB schemas ├─ workers/ │ ├─ chat-room.js # Autobase + blind-pairing room │ ├─ index.js # Bare chat worker entrypoint │ ├─ main.js # Bare updater worker (PearRuntime OTA) │ └─ worker-task.js # WorkerTask: Corestore + Hyperswarm + ChatRoom ├─ forge.config.js # Electron Forge makers + signing hooks ├─ input.css # Tailwind v4 entrypoint ├─ package.json # app metadata, scripts, deps ├─ pear.json # multisig namespace for OTA releases └─ schema.js # regenerates spec/ from schema definitions ``` The four "moving parts" are `electron/`, `workers/`, `spec/`, and `renderer/`. Everything else is plumbing or build config. Read the Bare chat worker [#read-the-bare-chat-worker] The worker is the **only** place P2P code lives. The renderer never imports [Hyperswarm](/reference/building-blocks/hyperswarm) or [Corestore](/reference/helpers/corestore); the main process barely touches them. That isolation keeps native modules out of the browser-style sandbox and off Electron's main thread. Open `workers/index.js`. It is the Bare-side entrypoint and is intentionally tiny: ```js file=/examples/getting-started/pear-chat/workers/index.js title="workers/index.js" lineNumbers {9-13,15-16,23-24,26-27,29-30,32-34,37-40} const FramedStream = require('framed-stream') const fs = require('bare-fs') const goodbye = require('graceful-goodbye') const { command, flag } = require('paparam') const path = require('bare-path') const WorkerTask = require('./worker-task.js') const cmd = command('pear-chat', flag('--invite|-i ', 'Room invite'), flag('--name|-n ', 'Your name'), flag('--reset', 'Reset') ) const storage = path.join(Bare.argv[2], 'corestore') cmd.parse(Bare.argv.slice(3)) async function main () { if (cmd.flags.reset) { await fs.promises.rm(storage, { recursive: true, force: true }) } const pipe = new FramedStream(Bare.IPC) pipe.pause() const workerTask = new WorkerTask(pipe, storage, cmd.flags) goodbye(() => workerTask.close()) await workerTask.ready() pipe.resume() console.log(`Storage: ${storage}`) console.log(`Name: ${workerTask.name}`) console.log(`Invite: ${await workerTask.room.getInvite()}`) } main().catch((err) => { console.error(err) Bare.exit(1) }) ``` Things to notice: * **Lines 9–13:** the worker declares its flags with [`paparam`](https://www.npmjs.com/package/paparam): `--invite`/`-i` (the room code a joining peer pastes), `--name`/`-n`, and `--reset`. * **Lines 15–16:** `Bare.argv[2]` is the storage directory the main process passes when it spawns the worker; the remaining flags are parsed on line 16. * **Lines 23–24:** the worker wraps [`Bare.IPC`](/reference/pear/runtime#running-workers) in a [`framed-stream`](https://www.npmjs.com/package/framed-stream), then pauses it until the task is ready. From here on, worker and renderer talk by exchanging JSON over this framed pipe—no typed RPC layer. * **Lines 26–27 and 29–30:** `WorkerTask` extends [`ready-resource`](https://www.npmjs.com/package/ready-resource) so open/close are explicit lifecycle steps. [`graceful-goodbye`](https://www.npmjs.com/package/graceful-goodbye) registers its `close()` for shutdown. Once ready, line 30 resumes the stream so buffered frames flow. * **Lines 32–34:** the `console.log` lines print the storage path, the chosen name, and the room invite—handy when running two peers from the terminal. Trace WorkerTask [#trace-workertask] Open `workers/worker-task.js`. This is the worker's "main object"—it owns a Corestore, a Hyperswarm, and a `ChatRoom`, and it speaks JSON to the renderer over the pipe: ```js file=/examples/getting-started/pear-chat/workers/worker-task.js title="workers/worker-task.js" lineNumbers {18-28,42-49,58-61,63-67} const Corestore = require('corestore') const debounce = require('debounceify') const Hyperswarm = require('hyperswarm') const ReadyResource = require('ready-resource') const ChatRoom = require('./chat-room') class WorkerTask extends ReadyResource { constructor (pipe, storage, opts = {}) { super() this.pipe = pipe this.storage = storage this.invite = opts.invite this.name = opts.name || `User ${Date.now()}` this.peers = 0 this.store = new Corestore(storage) this.swarm = new Hyperswarm() this.swarm.on('connection', (conn) => { this.store.replicate(conn) this._peers(1) conn.once('close', () => this._peers(-1)) }) this.room = new ChatRoom(this.store, this.swarm, this.invite) this.debounceMessages = debounce(() => this._messages()) this.room.on('update', () => this.debounceMessages()) } async _open () { await this.store.ready() await this.room.ready() this.pipe.on('data', async (data) => { let message try { message = JSON.parse(data) } catch { return } if (message.type === 'add-message') { await this.room.addMessage(message.text, { name: this.name, at: Date.now() }) } }) // Push the room invite so the renderer can show its "copy invite" button. this.pipe.write(JSON.stringify({ type: 'invite', invite: await this.room.getInvite() })) await this.debounceMessages() } async _close () { await this.room.close() await this.swarm.destroy() await this.store.close() } _peers (delta) { this.peers += delta this.pipe.write(JSON.stringify({ type: 'peers', count: this.peers })) } async _messages () { const messages = await this.room.getMessages() messages.sort((a, b) => a.info.at - b.info.at) this.pipe.write(JSON.stringify({ type: 'messages', messages })) } } module.exports = WorkerTask ``` The lifecycle is canonical Pear shape: * **Construct (L9–L29)**—wire dependencies, do not perform I/O. The [Corestore](/reference/helpers/corestore), [Hyperswarm](/reference/building-blocks/hyperswarm), and `ChatRoom` are created. On every swarm `connection` the store replicates and the peer count ticks (L20–L24); the room's `update` event is wired to a debounced `_messages()` push (L27–L28). * **`_open()` (L31–L50)**—open the store and room, subscribe to incoming renderer IPC (JSON parsed off the pipe, L35–L45), then push the room's invite (L48) and the first batch of messages (L49) to the renderer. * **`_peers()` (L58–L61)**—every connect/disconnect writes a `{ type: 'peers', count }` frame, which lights the renderer's status dot. * **`_messages()` (L63–L67)**—read every message from the room, sort by timestamp, and write a `{ type: 'messages', messages }` frame. Runs once on open and again, debounced, on every room `update`. The two additions over the bare scaffold are the **peer counter** and the **invite push** (L48)—the worker hands the renderer the room code so the UI can show a working "Copy invite" button. For why P2P state is owned by the worker rather than the renderer, read [Workers](/explanation/workers). Read ChatRoom [#read-chatroom] `workers/chat-room.js` is the actual peer-to-peer data structure. It combines four building blocks: * An [Autobase](/reference/building-blocks/autobase) so multiple writers (each peer's local core) merge into one deterministic materialised view. * A [HyperDB](https://www.npmjs.com/package/hyperdb) view (a [Hyperbee](/reference/building-blocks/hyperbee)-derived database) for the `messages` and `invites` tables. * [HyperDispatch](https://www.npmjs.com/package/hyperdispatch) for typed Autobase `append` payloads (`@pear-chat/add-message`, `@pear-chat/add-writer`, `@pear-chat/add-invite`). * [`blind-pairing`](https://www.npmjs.com/package/blind-pairing) so a new peer joins with only a short invite code—the inviter never sees the joiner's key in plaintext. These come together when peer **B** joins peer **A**'s room. **1. A creates an invite.** `getInvite()` appends a `@pear-chat/add-invite` record to the Autobase and returns a [z32](https://www.npmjs.com/package/z32)-encoded invite code (this is the string the worker pushes to the renderer's copy button): ```js file=/examples/getting-started/pear-chat/workers/chat-room.js#L127-L137 title="workers/chat-room.js" async getInvite () { const existing = await this.view.findOne('@pear-chat/invites', {}) if (existing) { return z32.encode(existing.invite) } const { id, invite, publicKey, expires } = BlindPairing.createInvite(this.base.key) await this.base.append( ChatDispatch.encode('@pear-chat/add-invite', { id, invite, publicKey, expires }) ) return z32.encode(invite) } ``` **2. B pairs as a candidate.** In `_open`, an empty local core *and* an `--invite` means the room uses [`blind-pairing`](https://www.npmjs.com/package/blind-pairing) as a candidate to reach **A** over the swarm and receive the Autobase keys: ```js file=/examples/getting-started/pear-chat/workers/chat-room.js#L37-L47 title="workers/chat-room.js" if (isEmpty && this.invite) { const res = await new Promise((resolve) => { this.pairing.addCandidate({ invite: z32.decode(this.invite), userData: localKey, onadd: resolve }) }) key = res.key encryptionKey = res.encryptionKey } ``` **3. A confirms and adds B as a writer.** A's `pairing.addMember` `onadd` handler resolves the invite, calls `addWriter(B.key)`, and hands back the Autobase root and encryption keys: ```js file=/examples/getting-started/pear-chat/workers/chat-room.js#L72-L85 title="workers/chat-room.js" this.pairMember = this.pairing.addMember({ discoveryKey: this.base.discoveryKey, /** @type {function(import('blind-pairing-core').MemberRequest)} */ onadd: async (request) => { const inv = await this.view.findOne('@pear-chat/invites', { id: request.inviteId }) if (!inv) return request.open(inv.publicKey) await this.addWriter(request.userData) request.confirm({ key: this.base.key, encryptionKey: this.base.encryptionKey }) } }) ``` **4. B opens the shared Autobase.** With those keys, **B** opens the base, joins the discovery topic on the swarm, and waits for the writable signal before replicating: ```js file=/examples/getting-started/pear-chat/workers/chat-room.js#L52-L68 title="workers/chat-room.js" this.base = new Autobase(this.store, key, { encrypt: true, encryptionKey, open: this._openBase.bind(this), close: this._closeBase.bind(this), apply: this._applyBase.bind(this) }) const writablePromise = new Promise((resolve) => { this.base.on('update', () => { if (this.base.writable) resolve() if (!this.base._interrupting) this.emit('update') }) }) await this.base.ready() this.swarm.join(this.base.discoveryKey) if (!this.base.writable) await writablePromise ``` Any message either peer adds is appended to its local Autobase core, replicated through the swarm, and applied to the shared HyperDB view. The room emits `update` on every Autobase update; `WorkerTask` debounces those into a single `{ type: 'messages', messages }` write back to the renderer. For the deeper picture of Autobase merging, read [From append-only logs to files](/explanation/from-logs-to-files). Understand spec/ [#understand-spec] The `spec/` directory holds **generated** code—that's why `npm run build` had to run before the first start. `schema.js` at the repo root regenerates everything in `spec/` from declarative definitions, in three blocks. `spec/schema/`—the canonical record shapes (`writer`, `invite`, `message`) registered with HyperSchema: ```js file=/examples/getting-started/pear-chat/schema.js#L9-L34 title="schema.js — records" const hyperSchema = Hyperschema.from(SCHEMA_DIR) const schema = hyperSchema.namespace('pear-chat') schema.register({ name: 'writer', fields: [ { name: 'key', type: 'buffer', required: true } ] }) schema.register({ name: 'invite', fields: [ { name: 'id', type: 'buffer', required: true }, { name: 'invite', type: 'buffer', required: true }, { name: 'publicKey', type: 'buffer', required: true }, { name: 'expires', type: 'int', required: true } ] }) schema.register({ name: 'message', fields: [ { name: 'id', type: 'string', required: true }, { name: 'text', type: 'string', required: true }, { name: 'info', type: 'json' } ] }) Hyperschema.toDisk(hyperSchema) ``` `spec/db/`—typed HyperDB collections (`@pear-chat/messages`, `@pear-chat/invites`), each keyed by `id`: ```js file=/examples/getting-started/pear-chat/schema.js#L36-L48 title="schema.js — collections" const hyperdb = HyperdbBuilder.from(SCHEMA_DIR, DB_DIR) const db = hyperdb.namespace('pear-chat') db.collections.register({ name: 'invites', schema: '@pear-chat/invite', key: ['id'] }) db.collections.register({ name: 'messages', schema: '@pear-chat/message', key: ['id'] }) HyperdbBuilder.toDisk(hyperdb) ``` `spec/dispatch/`—typed [Autobase](/reference/building-blocks/autobase) append-payload encoders, one per record type: ```js file=/examples/getting-started/pear-chat/schema.js#L50-L55 title="schema.js — dispatch" const hyperdispatch = Hyperdispatch.from(SCHEMA_DIR, DISPATCH_DIR, { offset: 0 }) const dispatch = hyperdispatch.namespace('pear-chat') dispatch.register({ name: 'add-writer', requestType: '@pear-chat/writer' }) dispatch.register({ name: 'add-invite', requestType: '@pear-chat/invite' }) dispatch.register({ name: 'add-message', requestType: '@pear-chat/message' }) Hyperdispatch.toDisk(hyperdispatch) ``` You do not normally edit files under `spec/`. To change a message shape or add a record type, edit `schema.js` and re-run `npm run build:db`. For why P2P apps benefit from schema-first design, see [Schema-first design](/explanation/workers#schema-first-design). Read the Electron main process [#read-the-electron-main-process] Open `electron/main.js`. The whole app is CommonJS—mirroring [`hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron). The main process keeps Electron's UI thread free of P2P work by spawning two [workers](/explanation/workers)—separate Bare processes—and brokering messages between them and the renderer. Chat worker [#chat-worker] The chat worker (`workers/index.js`) owns the Corestore, swarm, and `ChatRoom`. The main process spawns it and relays its byte streams to every window. Because `Bare.IPC` is a raw byte stream, the main process wraps the worker in a [`FramedStream`](https://www.npmjs.com/package/framed-stream) and forwards deframed frames: ```js file=/examples/getting-started/pear-chat/electron/main.js#L101-L138 title="electron/main.js — chat worker" function getWorker (specifier) { if (workers.has(specifier)) return workers.get(specifier) const storage = path.join(getStorageDir(), 'app-storage') const worker = PearRuntime.run(require.resolve('..' + specifier), [storage, ...passthroughArgs]) const pipe = new FramedStream(worker) function sendWorkerStdout (data) { process.stdout.write(data) sendToAll('pear:worker:stdout:' + specifier, data) } function sendWorkerStderr (data) { process.stderr.write(data) sendToAll('pear:worker:stderr:' + specifier, data) } function sendWorkerIPC (data) { sendToAll('pear:worker:ipc:' + specifier, data) } function onBeforeQuit () { pipe.destroy() } ipcMain.handle('pear:worker:writeIPC:' + specifier, (evt, data) => { return pipe.write(data) }) workers.set(specifier, pipe) pipe.on('data', sendWorkerIPC) worker.stdout.on('data', sendWorkerStdout) worker.stderr.on('data', sendWorkerStderr) worker.once('exit', (code) => { app.removeListener('before-quit', onBeforeQuit) ipcMain.removeHandler('pear:worker:writeIPC:' + specifier) pipe.removeListener('data', sendWorkerIPC) worker.stdout.removeListener('data', sendWorkerStdout) worker.stderr.removeListener('data', sendWorkerStderr) sendToAll('pear:worker:exit:' + specifier, code) workers.delete(specifier) }) app.on('before-quit', onBeforeQuit) return pipe } ``` `getWorker` spawns the worker with the `PearRuntime.run` shortcut and fans four named IPC channels out to every window (`pear:worker:ipc`, `:stdout`, `:stderr`, `:exit`), plus a `pear:worker:writeIPC` handler the renderer calls to send bytes back. Updater worker [#updater-worker] The updater worker (`workers/main.js`) runs the [`pear-runtime`](/reference/pear/runtime) OTA updater with its **own** swarm and Corestore, downloads new application drives from peers, and emits `updating`/`updated`. When the main process asks it to apply a downloaded release, it calls `pear.updater.applyUpdate()` and replies `pear:updateApplied`. Upstream ships this **exact** worker as the [`hello-pear-worker`](https://github.com/holepunchto/hello-pear-worker) package—here it's inlined so you can read what it does: ```js file=/examples/getting-started/pear-chat/workers/main.js title="workers/main.js — updater worker" lineNumbers {10-13,15-22,24-27,30-36,40-41,49-56} const PearRuntime = require('pear-runtime') // pear-runtime on desktop; pear-mobile on mobile const Hyperswarm = require('hyperswarm') const Corestore = require('corestore') const goodbye = require('graceful-goodbye') const FramedStream = require('framed-stream') const path = require('bare-path') const dir = require('bare-storage') const { isBareKit } = require('which-runtime') // mobile doesn't have the executable path (argv[0]) // and the worker entry path (argv[1]) in the workers argv's // ... to reuse the same worker in all platforms this logic is needed const argv = (index) => Bare.argv[index + (isBareKit ? 0 : 2)] 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 }) pear.updater.on('error', console.error) if (updaterConfig.updates !== false) { swarm.on('connection', (connection) => store.replicate(connection)) swarm.join(pear.updater.drive.core.discoveryKey, { client: true, server: false }) } console.log('Application storage:', pear.storage) pear.updater.on('updating', () => pipe.write('updating')) pear.updater.on('updated', () => pipe.write('updated')) goodbye(async () => { await swarm.destroy() await pear.close() await store.close() }) pipe.on('data', async (data) => { const message = data.toString() if (message === 'pear:applyUpdate') { await pear.ready() await pear.updater.applyUpdate() pipe.write('pear:updateApplied') } else console.log(message) }) pipe.write('Hello from worker') ``` Things to notice: * **Lines 10–13:** an `argv()` helper offsets `Bare.argv` so the same worker reads its arguments identically on desktop and on mobile (`bare-kit`), where the executable and entry paths aren't present. * **Lines 15–22:** the runtime config the host passed positionally—`updates`, `version`, `upgrade`, `name`, storage `dir`, and the running `app` path—reassembled into one object, with `dir` falling back to the persistent per-app directory. * **Lines 24–27:** the worker opens its **own** `FramedStream` pipe, [Corestore](/reference/helpers/corestore), and [Hyperswarm](/reference/building-blocks/hyperswarm), then constructs the `pear-runtime` updater over them—independent of the chat worker's swarm and store. * **Lines 30–36:** with updates enabled, it replicates the store on every connection and joins the update drive's discovery key as a client to pull new releases from seeding peers. * **Lines 40–41:** the updater's `updating` and `updated` events are forwarded to the main process as pipe messages. * **Lines 49–56:** on `pear:applyUpdate` from the main process, it awaits `pear.ready()`, applies the release, and replies `pear:updateApplied`. The main process does **not** embed `new PearRuntime({ ... })`; like the template, it keeps the updater in its own Bare worker so update traffic never blocks the chat. It only spawns the worker, wraps it in a `FramedStream`, and relays `pear:event:updating` / `pear:event:updated`: ```js file=/examples/getting-started/pear-chat/electron/main.js#L142-L179 title="electron/main.js — updater worker" function getUpdaterPipe () { if (updaterPipe) return updaterPipe const dir = getStorageDir() const appPath = getAppPath() const extension = isLinux ? '.AppImage' : isMac ? '.app' : '.msix' const worker = PearRuntime.run(require.resolve('..' + updaterSpecifier), [ updates, version, upgrade, productName + extension, dir, appPath ]) const pipe = new FramedStream(worker) function onData (data) { const message = data.toString() if (message === 'updating') sendToAll('pear:event:updating', 'updating') else if (message === 'updated') sendToAll('pear:event:updated', 'updated') } function onStderr (data) { process.stderr.write(data) } function onBeforeQuit () { pipe.destroy() } pipe.on('data', onData) worker.stderr.on('data', onStderr) worker.once('exit', () => { app.removeListener('before-quit', onBeforeQuit) pipe.removeListener('data', onData) worker.stderr.removeListener('data', onStderr) updaterPipe = null }) app.on('before-quit', onBeforeQuit) updaterPipe = pipe return pipe } ``` `applyUpdate` swaps the app on disk; `app:afterUpdate` relaunches the process. The renderer wires both into a button: ```js file=/examples/getting-started/pear-chat/electron/main.js#L215-L247 title="electron/main.js — apply + relaunch" ipcMain.handle('pear:applyUpdate', () => { const pipe = getUpdaterPipe() return new Promise((resolve) => { function onData (data) { if (data.toString() === 'pear:updateApplied') { pipe.removeListener('data', onData) resolve() } } pipe.on('data', onData) pipe.write('pear:applyUpdate') }) }) ipcMain.handle('pear:startWorker', (evt, filename) => { getWorker(filename) return true }) ipcMain.handle('app:afterUpdate', () => { if (isLinux && process.env.APPIMAGE) { app.relaunch({ execPath: process.env.APPIMAGE, args: [ '--appimage-extract-and-run', ...process.argv.slice(1).filter((arg) => arg !== '--appimage-extract-and-run') ] }) } else if (!isWindows) { app.relaunch() } app.quit() }) ``` Single-instance lock + deep links [#single-instance-lock--deep-links] `requestSingleInstanceLock` makes a `pear-chat://` deep link from the OS go to the running instance instead of spawning a second one. `setAsDefaultProtocolClient(protocol)` registers the scheme: ```js file=/examples/getting-started/pear-chat/electron/main.js#L253-L292 title="electron/main.js — single instance" app.setAsDefaultProtocolClient(protocol) app.on('open-url', (evt, url) => { evt.preventDefault() handleDeepLink(url) }) const lock = app.requestSingleInstanceLock() if (!lock) { app.quit() } else { app.on('second-instance', (evt, args) => { const url = args.find((arg) => arg.startsWith(protocol + '://')) if (url) handleDeepLink(url) }) app.whenReady().then(() => { createWindow() .then(() => getUpdaterPipe()) .catch((err) => { console.error('Failed to create window:', err) app.quit() }) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow().catch((err) => { console.error('Failed to create window:', err) }) } }) }) app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } }) } ``` Expose the bridge in electron/preload.js [#expose-the-bridge-in-electronpreloadjs] The renderer never sees the worker handle directly. It only sees `window.bridge`: ```js file=/examples/getting-started/pear-chat/electron/preload.js title="electron/preload.js" {8,16,27-39} const { contextBridge, ipcRenderer, webUtils } = require('electron') contextBridge.exposeInMainWorld('bridge', { pkg () { return ipcRenderer.sendSync('pkg') }, getPathForFile: (file) => webUtils.getPathForFile(file), writeClipboard: (text) => ipcRenderer.invoke('clipboard:write', text), applyUpdate: () => ipcRenderer.invoke('pear:applyUpdate'), appAfterUpdate: () => ipcRenderer.invoke('app:afterUpdate'), onPearEvent: (name, listener) => { const wrap = (evt, eventName) => listener(eventName) ipcRenderer.on('pear:event:' + name, wrap) return () => ipcRenderer.removeListener('pear:event:' + name, wrap) }, startWorker: (specifier) => ipcRenderer.invoke('pear:startWorker', specifier), onWorkerStdout: (specifier, listener) => { const wrap = (evt, data) => listener(new Uint8Array(data)) ipcRenderer.on('pear:worker:stdout:' + specifier, wrap) return () => ipcRenderer.removeListener('pear:worker:stdout:' + specifier, wrap) }, onWorkerStderr: (specifier, listener) => { const wrap = (evt, data) => listener(new Uint8Array(data)) ipcRenderer.on('pear:worker:stderr:' + specifier, wrap) return () => ipcRenderer.removeListener('pear:worker:stderr:' + specifier, wrap) }, onWorkerIPC: (specifier, listener) => { const wrap = (evt, data) => listener(new Uint8Array(data)) ipcRenderer.on('pear:worker:ipc:' + specifier, wrap) return () => ipcRenderer.removeListener('pear:worker:ipc:' + specifier, wrap) }, onWorkerExit: (specifier, listener) => { const wrap = (evt, code) => listener(code) ipcRenderer.on('pear:worker:exit:' + specifier, wrap) return () => ipcRenderer.removeListener('pear:worker:exit:' + specifier, wrap) }, writeWorkerIPC: (specifier, data) => { return ipcRenderer.invoke('pear:worker:writeIPC:' + specifier, data) } }) ``` This is the **single door** between the HTML renderer and the Bare workers. It is intentionally narrow—`startWorker`, `writeWorkerIPC`, `onWorkerIPC`, `onWorkerExit` for the worker, `onPearEvent` / `applyUpdate` / `appAfterUpdate` for OTA, and one extra this part adds: * `writeClipboard` (line 8), which the copy-invite button calls. * The renderer sends `{ type: 'add-message', text }` and receives `{ type: 'messages' }`, `{ type: 'peers' }`, and `{ type: 'invite' }`—all plain JSON over the bridge. For *why* this split exists, read [the process model](/explanation/pear-desktop-architecture#process-model). Read the renderer [#read-the-renderer] The renderer is **vanilla HTML + JavaScript**—no framework, no bundler—styled with [Tailwind CSS v4](https://tailwindcss.com/) compiled by the CLI (the `build:tailwind` script), not the part-1 CDN script. Two files matter. renderer/index.html [#rendererindexhtml] The static chat shell: * a header with the Pear logo, a peer-status dot, * a **Copy invite** button, * an "Update ready" button (hidden until OTA fires), * a scrollable message list, * a composer form, and * every class is a Tailwind utility scanned out of the markup at build time: ```html file=/examples/getting-started/pear-chat/renderer/index.html#L12-L47 title="renderer/index.html — header"

Pear Chat

0 peers
``` renderer/app.js [#rendererappjs] The entrypoint. It talks to the worker entirely through `window.bridge`. The composer sends `{ type: 'add-message', text }`; the worker pushes back `messages`, `peers`, and `invite` frames: ```js file=/examples/getting-started/pear-chat/renderer/app.js#L90-L105 title="renderer/app.js — worker IPC" bridge.startWorker(SPECIFIER) const offWorkerIPC = bridge.onWorkerIPC(SPECIFIER, (data) => { let message try { message = JSON.parse(decoder.decode(data)) } catch { return } if (message.type === 'messages') renderMessages(message.messages) else if (message.type === 'peers') renderPeers(message.count) else if (message.type === 'invite') { invite = message.invite copyEl.disabled = false } }) ``` The copy button writes the cached invite to the clipboard through the bridge: ```js file=/examples/getting-started/pear-chat/renderer/app.js#L73-L78 title="renderer/app.js — copy invite" copyEl.addEventListener('click', () => { if (!invite) return bridge.writeClipboard(invite) copyEl.textContent = 'Copied!' setTimeout(() => { copyEl.textContent = 'Copy invite' }, 1500) }) ``` And the OTA banner is driven entirely by `bridge.onPearEvent`—inert in development, live once [part 4](/getting-started/build-a-peer-to-peer-chat/update) ships a second version: ```js file=/examples/getting-started/pear-chat/renderer/app.js#L80-L88 title="renderer/app.js — OTA banner" // OTA update banner, driven by the updater worker via pear:event:* (see electron/main.js). updateEl.addEventListener('click', async () => { updateEl.disabled = true updateEl.textContent = 'restarting…' await bridge.applyUpdate() await bridge.appAfterUpdate() }) bridge.onPearEvent('updating', () => { versionEl.textContent = 'updating…' }) bridge.onPearEvent('updated', () => { updateEl.classList.remove('hidden') }) ``` Peer-supplied text is always written with `textContent`, never `innerHTML`, so a message can't inject markup. **The split is deliberate.** The renderer treats the worker like a remote API—and that's exactly what it is. Swap the worker specifier and the same plumbing works.
Run two peers locally [#run-two-peers-locally] [Corestore](/reference/helpers/corestore) takes an exclusive lock on its directory, so each peer needs its own `--storage`. From the project root: ```bash skip="desktop-gui" npm run build # user1: create the room + print an invite npm start -- --storage /tmp/pear-chat-user1 --name user1 ``` Watch the terminal for an `Invite: …` line—or click **Copy invite** in the window. Copy that z32-encoded string. In a second terminal, still in `pear-chat/`: ```bash skip="desktop-gui" npm start -- --storage /tmp/pear-chat-user2 --name user2 --invite ``` A second window opens. Within a few seconds the two peers pair via [`blind-pairing`](https://www.npmjs.com/package/blind-pairing), the second becomes a writer on the shared [Autobase](/reference/building-blocks/autobase), and both windows show the message history and tick the peer dot to green. Type in one window, hit Enter, and watch it appear in the other. Both peers persist messages under `--storage`. Close the apps, reopen them with the same paths, and the conversation is still there. To wipe a peer's local state: ```bash skip="desktop-gui" npm start -- --storage /tmp/pear-chat-user1 --name user1 --reset ``` `npm start` already forwards `--no-updates`, so the OTA updater stays inert in development—[part 4](/getting-started/build-a-peer-to-peer-chat/update) flips it on.
What you built [#what-you-built] A complete P2P desktop chat with persistence, pairing, OTA-update wiring, and three-platform packaging config—all on the canonical Pear + Electron template. | Layer | File | Concept | | ------------ | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | Distribution | `forge.config.js`, `build/`, `pear.json` | [Build desktop distributables](/how-to/operate-an-app/build-and-package/build-desktop-distributables) | | Shell | `electron/main.js`, `electron/preload.js` | [Pear desktop application architecture](/explanation/pear-desktop-architecture) | | Updater | `workers/main.js` (PearRuntime OTA in Bare) | [Pear OTA](/reference/pear/runtime) | | Worker | `workers/index.js`, `workers/worker-task.js` | [Workers](/explanation/workers) | | Room | `workers/chat-room.js` | [Autobase](/reference/building-blocks/autobase) + [blind-pairing](https://www.npmjs.com/package/blind-pairing) | | Storage | `Corestore`, `HyperDB` view | [Corestore](/reference/helpers/corestore), [Hyperbee](/reference/building-blocks/hyperbee) | | Transport | `electron/preload.js`, framed worker pipe | JSON messages over `window.bridge` IPC | | UI | `renderer/index.html`, `renderer/app.js` | Vanilla DOM + [Tailwind CSS](https://tailwindcss.com/); copy-invite + peer dot | Where to go next [#where-to-go-next] * **Continue the path:** [Ship your app](/getting-started/build-a-peer-to-peer-chat/ship) (part 3 of 4)—mint a `pear://` link, build per-OS distributables, and stage your first release. Then [Deploy over-the-air updates](/getting-started/build-a-peer-to-peer-chat/update) (part 4 of 4) ships a second version live and the OTA banner you wired here goes hot. Or pick a capability to add on top of this scaffold—each how-to under [the How To guides](/how-to) documents only the **delta** over this exact code: * [Add blind peering to a chat app](/how-to/blind-peering/add-blind-peering-to-a-chat-app)—keep the room online when its writers are offline. * [Add Keet identity to a chat app](/how-to/manage-identity/add-keet-identity-to-a-chat-app)—anchor messages to a portable identity key. * [Host multiple rooms in one chat app](/how-to/connect-to-peers/host-multiple-rooms-in-one-chat-app)—extend from one room to an account with many. * [Share files in a peer-to-peer app](/how-to/stream-and-share-media/share-files-in-a-peer-to-peer-app)—swap the room's HyperDB view for a [Hyperdrive](/reference/building-blocks/hyperdrive). Or zoom out to the conceptual picture: * [Pear desktop application architecture](/explanation/pear-desktop-architecture)—why a production app splits OTA updates, storage, and workers. * [Workers](/explanation/workers)—what a Bare worker is and why P2P state lives there. * [Release pipeline](/explanation/deployment-releasing-apps-p2p)—staging links, multisig, and the OTA loop. # Ship your app import { Steps, Step } from 'fumadocs-ui/components/steps' This is **part 3 of 4** in the [getting started path](/getting-started). You take the production-shaped app from [part 2](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app) and walk it through the first half of Pear's release pipeline: 1. [Minting a `pear://` link](#touch-set-the-upgrade-link-and-seed) 2. [Building per-OS distributables](#make-distributables) 3. [Staging and provisioning the first version onto those links](#stage-and-provision-the-first-version) [Deploy over-the-air updates](/getting-started/build-a-peer-to-peer-chat/update) (part 4 of 4) continues from here and demonstrates the live OTA cycle on the provision link, plus a tour of multisig. The full pipeline looks like this: This part stops after step 6—the first `pear stage` plus `pear provision`. You do not need cosigners, a Windows machine, or Apple signing credentials. The deeper guides cover the production-only material: * [Deploy a Pear desktop app](/how-to/operate-an-app/manual-deployment/deployment)—every command, every release line. * [Build desktop distributables](/how-to/operate-an-app/build-and-package/build-desktop-distributables)—code-signing, notarization, MSIX publisher details. * [Release pipeline](/explanation/deployment-releasing-apps-p2p)—the conceptual picture. {/* Shared snippet (content/_snippets/) included via Fumadocs by every getting-started step. Reminds readers that a production-ready reference implementation already exists upstream, so they can clone it any time without abandoning the type-along. See: https://www.fumadocs.dev/docs/markdown#include */} **Full production-ready reference: `hello-pear-electron`.** The complete version of this chat lives at [`holepunchto/hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron)—Holepunch's official Electron template, the same shape [Keet](https://keet.io) and [PearPass](https://pass.pears.com) ship. Clone it any time to see the finished structure or to crib code. For a guided tour of the template, see [Start from the hello-pear-electron template](/getting-started/from-a-template/start-from-hello-pear-electron). Before you start [#before-you-start] You need: 1. The working production-shaped app from [part 2](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app). 2. [`pear`](/reference/pear/cli): * Install it with `npm i -g pear` or run via `npx pear`. * Every `pear` command in this part also works as `npx pear ...`. This is useful if you want to run the commands from a different directory than the one you installed `pear` in. 3. `pear build`—assembles per-OS makes into a deployment directory. It ships with the `pear` CLI above, so there is nothing extra to install: run it as `pear build` (or `npx pear build`). Touch, set the upgrade link, and seed [#touch-set-the-upgrade-link-and-seed] [`pear touch`](/reference/pear/cli) mints a **new `pear://` link you own**—one backed by a fresh [Hypercore](/reference/building-blocks/hypercore) whose write key is stored locally on this machine. This becomes your **stage link**: the append-only core you sync builds into before each provision. (The link you set in part 2 was a public placeholder so the app would boot; you replace it now with your own.) ```bash pear touch # pear:// ``` Stage and provision only work against links you **own**—ones you minted on this machine with `pear touch`. If you stage to someone else's link (for example the public placeholder from part 2), `pear stage` prints the header and then fails with `✖ Destination must be writable`. Always use the link `pear touch` just printed. Point `package.json#upgrade` at the stage link for now—you will switch it to the **provision link** after the first provision: ```bash npm pkg set upgrade=pear:// ``` [`pear seed`](/reference/pear/cli#pear-seed) keeps that core online so other peers can fetch updates from you. Your output will be different. ```bash pear seed pear:// Seeding: pear:// Drive Key: Drive Length: 0 Discovery Key: qx138e5wnc3bmnjcbks68xpp17m6bj6h5165hud9ps4tr8zad4fo Content Key: pending Firewalled: true NAT Type: consistent Whoami: t8kgj1p3a4e9x8p1etsgks1rc4j58yu9xjh11bbjkgys7zkfmabo Network: [ Peers 0 ] [ ⬆ 0B - 0B/s ] [ ⬇ 0B - 0B/s ] ──────────────────────────────────────────────────────── ^_^ announced ``` Leave `pear seed` running in its own terminal for the rest of the tutorial—without an active seeder, peers cannot download your build. In production, run `pear seed` on at least one always-online machine (a small VPS works) so updates keep flowing while developer laptops sleep. Bump the version [#bump-the-version] [`pear-runtime`](/reference/pear/runtime) only swaps the application drive when the new build advertises a higher version. If you forget this step, peers see your stage and do nothing: ```bash npm version [ | major | minor | patch | premajor | preminor | prepatch | prerelease] ``` This rewrites `package.json` (`1.0.0` → `1.0.1`) and creates a git tag. From now on, every release iteration starts with `npm version patch` (or `major`, `minor`, `patch`, etc.). Make distributables [#make-distributables] A "distributable" is the platform-native installer—`.app` on macOS, `.msix` on Windows, `.AppImage` on Linux. Pear uses [`electron-forge`](https://www.electronforge.io/) with a single [`forge.config.js`](https://github.com/holepunchto/hello-pear-electron/blob/main/forge.config.js) that configures makers for every platform, including Linux AppImage via [`pear-electron-forge-maker-appimage`](https://www.npmjs.com/package/pear-electron-forge-maker-appimage). Install electron-forge and the makers you need [#install-electron-forge-and-the-makers-you-need] ```bash npm install --save-dev \ @electron-forge/cli@^7.11.1 \ @electron-forge/maker-dmg@^7.11.1 \ @electron-forge/maker-msix@^7.11.1 \ pear-electron-forge-maker-appimage@^2.0.0 \ pear-electron-forge-maker-flatpak@^0.0.6 \ pear-electron-forge-maker-snap@^1.0.0 \ electron-forge-plugin-universal-prebuilds@^1.0.0 \ electron-forge-plugin-prune-prebuilds@^1.0.0 ``` Two electron-forge plugins matter: * [`electron-forge-plugin-universal-prebuilds`](https://www.npmjs.com/package/electron-forge-plugin-universal-prebuilds)—bundles native prebuilds for every supported architecture. * [`electron-forge-plugin-prune-prebuilds`](https://www.npmjs.com/package/electron-forge-plugin-prune-prebuilds)—trims the prebuilds you do not need for the current platform, keeping installers small. Add scripts to package.json [#add-scripts-to-packagejson] Use one make entry point on every OS—Forge runs only the makers whose `platforms` match the host: ```json "scripts": { "start": "electron-forge start -- --no-updates", "package": "electron-forge package", "make": "electron-forge make" } ``` Add forge.config.js [#add-forgeconfigjs] In your project's root, copy or diff against the canonical [`hello-pear-electron` `forge.config.js`](https://github.com/holepunchto/hello-pear-electron/blob/main/forge.config.js). That file configures: * **Packager**: `build/icon`, URL schemes from `package.json` `name`, optional macOS signing when `MAC_CODESIGN_IDENTITY` is set (notarization via `KEYCHAIN_PROFILE`). * **Makers**: `@electron-forge/maker-dmg` (darwin), `@electron-forge/maker-msix` (win32), and Pear makers for Linux AppImage, Flatpak, and Snap. * **Hooks**: `preMake` rewrites `build/AppxManifest.xml` version for MSIX; `postMake` moves Windows `.msix` artifacts into `out/-win32-/`. * **Plugins**: universal-prebuilds and prune-prebuilds. ```js file=/examples/getting-started/pear-chat/forge.config.js title="forge.config.js" const fs = require('fs') const path = require('path') const plink = require('pear-link') const pkg = require('./package.json') const appName = pkg.productName ?? pkg.name function getWindowsKitVersion () { const programFiles = process.env['PROGRAMFILES(X86)'] || process.env.PROGRAMFILES if (!programFiles) return undefined const kitsDir = path.join(programFiles, 'Windows Kits') try { for (const kit of fs.readdirSync(kitsDir).sort().reverse()) { const binDir = path.join(kitsDir, kit, 'bin') if (!fs.existsSync(binDir)) continue const version = fs .readdirSync(binDir) .filter((d) => /^\d+\.\d+\.\d+\.\d+$/.test(d)) .sort() .pop() if (version) return version } } catch { return undefined } } let packagerConfig = { icon: 'build/icon', protocols: [{ name: appName, schemes: [pkg.name] }], derefSymlinks: true } if (process.env.MAC_CODESIGN_IDENTITY) { packagerConfig = { ...packagerConfig, osxSign: { identity: process.env.MAC_CODESIGN_IDENTITY, optionsForFile: () => ({ entitlements: path.join(__dirname, 'build', 'entitlements.mac.plist') }) }, osxNotarize: { tool: 'notarytool', keychainProfile: process.env.KEYCHAIN_PROFILE } } } module.exports = { packagerConfig, makers: [ { name: '@electron-forge/maker-dmg', platforms: ['darwin'], config: {} }, { name: '@electron-forge/maker-msix', platforms: ['win32'], config: { appManifest: path.join(__dirname, 'build', 'AppxManifest.xml'), windowsKitVersion: getWindowsKitVersion(), ...(process.env.WINDOWS_SIGN_HOOK ? { windowsSignOptions: { hookModulePath: process.env.WINDOWS_SIGN_HOOK } } : {}) } }, { name: 'pear-electron-forge-maker-appimage', platforms: ['linux'], config: { icons: [ { file: 'build/icon/icon-16x16.png', size: 16 }, { file: 'build/icon/icon-32x32.png', size: 32 }, { file: 'build/icon/icon-64x64.png', size: 64 }, { file: 'build/icon/icon-128x128.png', size: 128 }, { file: 'build/icon/icon-256x256.png', size: 256 } ] } }, { name: 'pear-electron-forge-maker-flatpak', platforms: ['linux'], config: { appId: 'com.pears.BasicChat', icon: `${packagerConfig.icon}.png`, metainfo: 'build/metainfo.xml', entrypoint: 'build/entrypoint.sh', comment: 'A peer-to-peer chat example built with Pear and Electron', categories: ['Network', 'InstantMessaging'] } }, { name: 'pear-electron-forge-maker-snap', platforms: ['linux'], config: { snapcraftYamlPath: 'build/snapcraft.yaml', summary: 'A peer-to-peer chat example built with Pear and Electron', description: 'A peer-to-peer chat example demonstrating how to embed pear-runtime into an Electron desktop app.', contact: 'hello@holepunchto.to', license: 'Apache-2.0', issues: 'https://github.com/holepunchto/examples-p2p-desktop/issues', website: 'https://github.com/holepunchto/examples-p2p-desktop', icon: `${packagerConfig.icon}.png` } } ], hooks: { readPackageJson: async (forgeConfig, packageJson) => { if (process.env.UPGRADE_KEY) { packageJson.upgrade = process.env.UPGRADE_KEY } try { plink.parse(packageJson.upgrade) } catch { throw new Error('Use `pear touch` to get a valid upgrade key for package.json#upgrade') } return packageJson }, preMake: async () => { fs.rmSync(path.join(__dirname, 'out', 'make'), { recursive: true, force: true }) const manifest = path.join(__dirname, 'build', 'AppxManifest.xml') const msixVersion = pkg.version.replace(/^(\d+\.\d+\.\d+)$/, '$1.0') const xml = fs.readFileSync(manifest, 'utf-8') fs.writeFileSync(manifest, xml.replace(/Version="[^"]*"/, `Version="${msixVersion}"`)) }, postMake: async (forgeConfig, results) => { for (const result of results) { if (result.platform !== 'win32') continue for (const artifact of result.artifacts) { if (!artifact.endsWith('.msix')) continue const standardDir = path.join(__dirname, 'out', `${appName}-win32-${result.arch}`) fs.mkdirSync(standardDir, { recursive: true }) const dest = path.join(standardDir, path.basename(artifact)) fs.renameSync(artifact, dest) fs.mkdirSync(path.dirname(artifact), { recursive: true }) fs.copyFileSync(dest, artifact) result.artifacts[result.artifacts.indexOf(artifact)] = dest } } } }, plugins: [ { name: 'electron-forge-plugin-universal-prebuilds', config: {} }, { name: 'electron-forge-plugin-prune-prebuilds', config: {} } ] } ``` Add the template build/ assets to your project [#add-the-template-build-assets-to-your-project] Createa a `build/` folder in your project's root and copy the template's `build/` assets (`AppxManifest.xml`, `entitlements.mac.plist`, icon set under `build/icon/`, and Linux Flatpak/Snap metadata). Run the maker on your OS [#run-the-maker-on-your-os] `npm run make` builds distributables for the **current host OS** only. Run it on macOS, Linux, and Windows (or on CI runners per OS) when you need a multi-arch deployment directory. Brand icons before you make: * `build/icon.icns` (macOS), * `build/icon.ico` (Windows), * `build/icon.png` plus sized PNGs under `build/icon/` (Linux makers). You can copy the set from [`hello-pear-electron`'s `build/` tree](https://github.com/holepunchto/hello-pear-electron/tree/main/build). ```bash npm run make # .app + .dmg on macOS; .AppImage (+ Flatpak/Snap) on Linux; .msix on Windows ``` The output lands in `out/PearChat-darwin-arm64/PearChat.app` (or the matching path for your platform). If the make fails with a `NODE_MODULE_VERSION` mismatch (for example after `nvm use` or upgrading Node between `npm install` and `npm run make`), run `npm rebuild` and try again. See [Node ABI mismatch during make](/how-to/operate-an-app/manual-deployment/troubleshoot-desktop-releases#node-abi-mismatch-during-make). Code-signing, notarization, and MSIX publisher requirements are full topics on their own—production builds need them, but you can skip them for this dry run. The full coverage is in [Build desktop distributables](/how-to/operate-an-app/build-and-package/build-desktop-distributables). See [Desktop release npm scripts](/reference/ci-and-release/desktop-release-npm-scripts) for common `npm` entry points in sample repos. Build the deployment directory [#build-the-deployment-directory] You should run `pear build` from **outside** the project folder (`pear build` and the project folder must not be parent/child—see [stage size increases](https://github.com/holepunchto/hello-pear-electron#stage-size-increases)). Each `---app` flag points at one make's output. For example: ```bash cd .. pear build \ --package=./pear-chat/package.json \ --darwin-arm64-app ./pear-chat/out/PearChat-darwin-arm64/PearChat.app \ --target pear-chat-1.0.1 ``` The result is `./pear-chat-1.0.1/by-arch/darwin-arm64/app/...` ready for the next step. If you have makes from more than one platform—for example a Linux AppImage built on a colleague's machine—pass each one: ```bash pear build \ --package=./pear-chat/package.json \ --darwin-arm64-app ./pear-chat/out/PearChat-darwin-arm64/PearChat.app \ --linux-x64-app ./pear-chat/out/PearChat-linux-x64/PearChat.AppImage \ --win32-x64-app ./pear-chat/out/PearChat-win32-x64/PearChat.msix \ --target pear-chat-1.0.1 ``` `pear build` assembles a Pear deployment directory from the per-platform makes. The layout must be as follows: ```text PearChat-1.0.1/ ├─ package.json └─ by-arch/ └─ / └─ app/ ``` Stage and provision the first version [#stage-and-provision-the-first-version] Run these from the **parent directory** of your app (same place you ran `pear build`) unless noted. [`pear stage`](/reference/pear/cli) syncs the deployment directory into the [Hypercore](/reference/building-blocks/hypercore) behind your **stage link**. 1. Dry-run the stage [#1-dry-run-the-stage] Always run `--dry-run` first and read the file-by-file diff: ```bash pear stage --dry-run pear:// ./pear-chat-1.0.1 ``` Your keys and byte counts will differ; the shape looks like this: ```text 🍐 Staging pear-chat [ pear:// ] pear://0.0. Current: 1 NOTE: This is a dry run, no changes will be persisted. + /package.json (+2.1kB) + /by-arch/darwin-arm64/app/PearChat.app/Contents/Info.plist (+1.4kB) + /by-arch/darwin-arm64/app/PearChat.app/Contents/MacOS/PearChat (+55kB) + /by-arch/darwin-arm64/app/PearChat.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework (+165MB) + /by-arch/darwin-arm64/app/PearChat.app/Contents/Resources/app/electron/main.js (+12.4kB) + /by-arch/darwin-arm64/app/PearChat.app/Contents/Resources/app/workers/chat-room.js (+8.7kB) + /by-arch/darwin-arm64/app/PearChat.app/Contents/Resources/app/renderer/index.html (+3.2kB) ... more files under /by-arch/ ... ✔ Skipping (dry-run) Staging dry run complete! ``` How to read it: * **`+` lines**—files that would be written or updated, as `/`-prefixed drive paths with byte counts in parentheses. Scan for surprises before you run without `--dry-run`. * **Versioned link in the header**—the line under the bracketed stage link shows the drive at its **current** length. On a first stage nothing is staged yet, so it reads `pear://0.0.`. The versioned link you need for `pear provision` is printed by the real stage in the next step, after the sync actually happens. Look for: * Only two entries at the drive root—`/package.json` and `/by-arch/...`. That is everything `pear build` writes into the deployment directory. Your app source (`electron/`, `workers/`, `renderer/`, `node_modules/`) ships **inside** the packaged app—for example under `/by-arch/darwin-arm64/app/PearChat.app/Contents/Resources/app/`—never as loose files at the root. * No surprise additions—stray `.DS_Store`, editor swap files, secrets, the deployment directory itself. * Sensible byte counts—the Electron framework binary dominates (a hundred-plus MB per platform-arch); if an unrelated file is suddenly 100 MB, something is wrong. 2. Stage for real [#2-stage-for-real] If the diff looks right, drop the `--dry-run` flag and run it for real: ```bash pear stage pear:// ./pear-chat-1.0.1 ``` The live output repeats the dry-run diff, then ends with the post-stage **versioned link** after the `^Latest:` line: ```text 🍐 Staging pear-chat [ pear:// ] pear://0.0. Current: 1 + /package.json (+2.1kB) ... same diff as the dry run ... Staging complete! ^Latest: 1426 pear://0.1426. [ pear:// ] ``` Copy the versioned link printed under `^Latest:`—`pear://0..`, where `` is the Hypercore length **after** this stage (a few hundred to a few thousand on a first stage). Don't copy the versioned link at the top of the output—that one shows the drive **before** staging (`pear://0.0.` on a first stage). Use the full versioned string as the first argument to `pear provision` below (not the unversioned `` you passed to `pear stage`). 3. Mint the provision link [#3-mint-the-provision-link] Peers should not poll the stage link directly—it keeps the full append-only history. [`pear provision`](/reference/pear/cli#pear-provision) block-syncs the staged snapshot onto a lean **provision link** that packaged apps poll instead. That target is a **second** link—mint it now with another `pear touch` (the first touch in step 1 was only for staging). Cwd does not matter; `pear touch` does not read your project: ```bash pear touch # pear:// ← different key from ``` `pear touch` prints the link on its own line—copy the whole `pear://…` string. 4. Provision onto the provision link [#4-provision-onto-the-provision-link] Dry-run first, then run for real. Fill the three arguments like this: | Argument | What to paste | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `` | The versioned link from `pear stage`: `pear://0..` | | `` | The unversioned provision link from `pear touch`: `pear://` | | `` | On first ship: `pear://0.0.` where `` is everything after `pear://` in `` | Paste all three links on **one line**—backslash continuations are easy to break if a line has trailing spaces: ```bash pear provision --dry-run pear://0.. pear:// pear://0.0. pear provision pear://0.. pear:// pear://0.0. ``` Example with concrete keys (yours will differ): ```bash pear provision --dry-run pear://0.1426.9mdt6h676phg7nuwp1urs7457nsz3juyeuqcbw4h7r1tqz8e84ay pear://o11z79iogfzx1ckthrhwoeyk7pyxxhy7par4edq5sy1dmwieutso pear://0.0.o11z79iogfzx1ckthrhwoeyk7pyxxhy7par4edq5sy1dmwieutso ``` Provision syncs the target, prints the same file diff you just staged plus a summary, and—on the real run—ends with the provisioned links. The dry run stops after `Dry Run Complete`; the real run pauses for a 10-second cooldown before writing: ```text Syncing existing metadata, please wait... Completed metadata sync Checking diff + /package.json (+2.1kB) ... same diff as the stage ... Diffing complete Total changes: 1425 Package version: 1.0.1 Core: Key: o11z79iogfzx1ckthrhwoeyk7pyxxhy7par4edq5sy1dmwieutso ... NOT A DRY RUN! Waiting 10s for certainty. Use ctrl+c to bail Staging to target... ... same diff again ... Provisioned: Verlink: pear://0.1426.o11z79iogfzx1ckthrhwoeyk7pyxxhy7par4edq5sy1dmwieutso Hashlink: pear://0.1426.o11z79iogfzx1ckthrhwoeyk7pyxxhy7par4edq5sy1dmwieutso. Seed with: pear seed pear://o11z79iogfzx1ckthrhwoeyk7pyxxhy7par4edq5sy1dmwieutso ``` The diff should mirror what you just staged. The `Seed with` line at the end names the unversioned provision link peers will poll—set `upgrade` to that link in the next step, not the versioned stage link. 5. Switch upgrade to the provision link [#5-switch-upgrade-to-the-provision-link] From the **app root** (`pear-chat/`), point `package.json#upgrade` at the provision link and seed it—this is what shipped binaries poll: ```bash npm pkg set upgrade=pear:// pear seed pear:// ``` 6. Rebuild once [#6-rebuild-once] Rebuild so the packaged app embeds the new `upgrade` field (the provision link already holds v1.0.1—no restage needed): ```bash npm run make cd .. pear build \ --package=./pear-chat/package.json \ --darwin-arm64-app ./pear-chat/out/PearChat-darwin-arm64/PearChat.app \ --target pear-chat-1.0.1 ``` Your first version is now published on the provision link. Peers running a build with that `upgrade` field will see it on their next poll. Keep both links: **stage** for `pear stage` on every iteration; **provision** for what apps install and poll. Part 4 repeats `pear stage` → `pear provision` for v1.0.2. The full operator reference is [Deploy your application—provision](/how-to/operate-an-app/manual-deployment/deployment#6-provision). What you've learned [#what-youve-learned] You now have a stage link and a provision link with your first build published: | Stage | What it is | Reversible? | | ------------------- | --------------------------------------------------------------------------------- | --------------------------------------------- | | `pear touch` | Mints a new `pear://` link | Yes—just abandon it | | Make + `pear build` | Per-OS distributable folded into a Deployment Directory | Yes—rebuild | | `pear stage` | Append-only sync into the stage [Hypercore](/reference/building-blocks/hypercore) | History is permanent; updates are not | | `pear provision` | Block-sync onto the provision link peers poll | Yes—reprovision from a different stage length | Every release iteration after this is the same pattern: `npm version patch`, `npm run make` (on each OS you ship), `pear build`, `pear stage --dry-run`, `pear stage`, `pear provision`. Part 4 puts that loop on a running app and shows the OTA cycle from both sides. Where to go next [#where-to-go-next] * **Continue the path:** [Deploy over-the-air updates](/getting-started/build-a-peer-to-peer-chat/update) (part 4 of 4)—run the installed build, ship a second version, watch OTA fire end-to-end, and preview multisig. * **Automate it:** [Publish with GitHub Actions](/how-to/operate-an-app/github-actions/publish-with-github-actions)—skip the manual staging and let CI stage a stable `pear://` link on every push. * [Deploy a Pear desktop app](/how-to/operate-an-app/manual-deployment/deployment)—the canonical how-to with every command, every flag, and every recovery procedure. * [Build desktop distributables](/how-to/operate-an-app/build-and-package/build-desktop-distributables)—code-signing, notarization, MSIX publisher details. * [Publish a changelog for your app](/how-to/operate-an-app/publish-a-changelog)—ship a `CHANGELOG.md` so users read your release notes with `pear changelog`. * [Troubleshoot desktop releases](/how-to/operate-an-app/manual-deployment/troubleshoot-desktop-releases)—"the app did not update," lost write-access, stage size blowups. * [Release pipeline](/explanation/deployment-releasing-apps-p2p)—the conceptual picture, deployment layers, and release lines. * [Release pipeline glossary](/explanation/deployment-releasing-apps-p2p#glossary)—terminology. * [`hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron)—the upstream template every snippet in this getting started path is based on. # Deploy over-the-air updates import { Steps, Step } from 'fumadocs-ui/components/steps' This is **part 4 of 4** in the [getting started path](/getting-started). You start from the v1.0.1 you shipped in [part 3](/getting-started/build-a-peer-to-peer-chat/ship): * a [**provision link**](/getting-started/build-a-peer-to-peer-chat/ship#5-switch-upgrade-to-the-provision-link) in `package.json#upgrade` * an active [`pear seed`](/getting-started/build-a-peer-to-peer-chat/ship#5-switch-upgrade-to-the-provision-link) on that link * a [packaged build](/getting-started/build-a-peer-to-peer-chat/ship#6-rebuild-once) that embeds the same field. From here you put the live OTA cycle to work: 1. [Run the installed app](#open-the-installed-build) 2. [Change something visible in the renderer](#make-a-visible-change) 3. [Stage v1.0.2 onto your **stage link**](#stage-and-provision-the-deployment-directory) 4. [Provision it onto the provision link](#stage-and-provision-the-deployment-directory) 5. [Watch the OTA cycle fire](#watch-the-ota-cycle-fire)—see the [OTA update event lifecycle](/explanation/deployment-releasing-apps-p2p#ota-update-event-lifecycle) 6. [Preview multisig](#multisig-production-releases), the production gate on top of provision. {/* Shared snippet (content/_snippets/) included via Fumadocs by every getting-started step. Reminds readers that a production-ready reference implementation already exists upstream, so they can clone it any time without abandoning the type-along. See: https://www.fumadocs.dev/docs/markdown#include */} **Full production-ready reference: `hello-pear-electron`.** The complete version of this chat lives at [`holepunchto/hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron)—Holepunch's official Electron template, the same shape [Keet](https://keet.io) and [PearPass](https://pass.pears.com) ship. Clone it any time to see the finished structure or to crib code. For a guided tour of the template, see [Start from the hello-pear-electron template](/getting-started/from-a-template/start-from-hello-pear-electron). You do not need cosigners, a Windows machine, or Apple signing credentials to follow along. The deeper guides cover the production-only material: * [Deploy a Pear desktop app](/how-to/operate-an-app/manual-deployment/deployment)—every command, every release line. * [Build desktop distributables](/how-to/operate-an-app/build-and-package/build-desktop-distributables)—code-signing, notarization, MSIX publisher details. * [Release pipeline](/explanation/deployment-releasing-apps-p2p)—the conceptual picture. Before you start [#before-you-start] You need: 1. The shipped v1.0.1 from [part 3—ship](/getting-started/build-a-peer-to-peer-chat/ship). That part covered `pear touch`, the stage and provision links, `npm version patch`, `electron-forge make`, `pear build`, `pear stage`, and `pear provision`. 2. The `pear seed` on your **provision link** from part 3 still running. Without an active seeder, peers cannot fetch the new version. 3. The same `pear` CLI from part 3—`pear build` ships with it (`npm i -g pear` or `npx pear ...`). Open the installed build [#open-the-installed-build] ```bash open ./pear-chat/out/PearChat-darwin-arm64/PearChat.app # Linux: ./pear-chat/out/PearChat-linux-x64/PearChat.AppImage # Windows: install PearChat-1.0.1.msix and launch from Start menu ``` The header shows `v1.0.1`. Send a few chat messages so you can confirm the transcript survives the restart. Leave the app open. This must be the **installed `.app`**, not `npm start`. The dev script forwards `--no-updates`, which intentionally disables OTA. Only the packaged build polls for updates. On macOS, the first launch may prompt Gatekeeper because this build is unsigned. Right-click the `.app` → **Open** → **Open** to bypass it once. Production signing and notarization is covered in [Build desktop distributables](/how-to/operate-an-app/build-and-package/build-desktop-distributables). Make a visible change [#make-a-visible-change] Edit `renderer/index.html` and change the header so the new version is obviously different: ```diff -

Pear chat

+

Pear chat — v2

``` Any visible tweak works (text, color, emoji). The point is to confirm the renderer asset on disk swaps after the update.
Bump, make, build, stage, provision [#bump-make-build-stage-provision] Same release cycle as part 3, with a patch bump. Run from your terminal in the project root—your packaged app keeps running in the GUI: Bump the version [#bump-the-version] ```bash npm version [ | major | minor | patch | premajor | preminor | prepatch | prerelease] ``` Note the change in the changelog [#note-the-change-in-the-changelog] Add an entry for the new version to `CHANGELOG.md` at your project root—create the file if this is your first release note, and start it with a title line before the first `##` heading: ```md title="CHANGELOG.md" # Pear chat changelog ## v1.0.2 ### Changes - Renamed the header to "Pear chat — v2". ``` Keep releases newest-first with a SemVer version as the first word of each `##` heading. `pear build` copies only `package.json` and `by-arch/` into the deployment directory, so you copy this file in before staging (below); peers then read it with `pear changelog pear://`. See [Publish a changelog for your app](/how-to/operate-an-app/publish-a-changelog) for the format details. Make the distributables [#make-the-distributables] ```bash npm run make ``` Build the deployment directory [#build-the-deployment-directory] ```bash cd .. pear build \ --package=./pear-chat/package.json \ --darwin-arm64-app ./pear-chat/out/PearChat-darwin-arm64/PearChat.app \ --target pear-chat-1.0.2 ``` Stage and provision the deployment directory [#stage-and-provision-the-deployment-directory] ```bash cp ./pear-chat/CHANGELOG.md ./pear-chat-1.0.2/ # release notes ride along with the build pear stage pear:// ./pear-chat-1.0.2 pear provision \ pear://0.. \ pear:// \ pear://0.0. ``` Use the versioned link from the `pear stage` output for the first argument. `` is the key segment from `` (everything after `pear://`), same as in [part 3](/getting-started/build-a-peer-to-peer-chat/ship#stage-and-provision-the-first-version). On every cycle, the `--target` value and the deployment dir you pass to `pear stage` must match the version you just bumped to. The example above assumes you went from `1.0.1` to `1.0.2`; if you've already iterated past that, use `pear-chat-` everywhere. Otherwise `pear build` writes v`X` into a directory named after v`Y`, which makes the next bump confusing to debug. You can also omit `--target` and `pear build` will auto-name the dir as `-` from `package.json`. Watch the OTA cycle fire [#watch-the-ota-cycle-fire] Within seconds the running app reacts. The events come from part 2's wiring (`pear.updater.on('updating'|'updated')` → preload bridge → renderer): 1. The version label in the header flips from `v1.0.1` to `updating…`. 2. The yellow **Update ready** button appears. 3. Click it. `applyUpdate()` swaps the application drive, `appAfterUpdate()` restarts the process. 4. The header now reads **"Pear chat—v2"** and the version label shows `v1.0.2`. The [Corestore](/reference/helpers/corestore)-backed chat transcript replays from disk. If nothing happens for more than a minute, check that `pear seed` is still running on your **provision link** and that `upgrade` in `package.json` matches ``—see [App did not update](/how-to/operate-an-app/manual-deployment/troubleshoot-desktop-releases#app-did-not-update). Part 2 wires the runtime with `delay: 0`, so updates fire as soon as the new content reaches the local drive. The default `pear-runtime-updater` delay is a random value up to **one hour** after the 60s boot grace period—great for production (seeders avoid a thundering herd) but invisible in a tutorial. If you keep that default and the **Update ready** button never appears, restart the app to reset the grace period—see [Tune `PearRuntime` `delay` for live OTA visibility](/how-to/operate-an-app/manual-deployment/troubleshoot-desktop-releases#tune-pearruntime-delay-for-live-ota-visibility). The walkthrough stops here. Multisig changes real production links and requires coordinating with at least one other signer, so a tutorial walkthrough is the wrong format. The next section summarizes what it does and links to the production guides. Why stage and provision are separate links [#why-stage-and-provision-are-separate-links] A **stage link** keeps the full append-only history—every file you ever staged, even ones you later deleted. A **provision link** is the lean snapshot peers poll: [`pear provision`](/reference/pear/cli#pear-provision) block-syncs from a versioned stage link onto the provision target, compacting deletions along the way. Part 3 and the iterate loop above already use that split; [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment#6-provision) covers recovery if a stage or provision link is lost. Multisig (production releases) [#multisig-production-releases] A multisig drive is a [Hypercore](/reference/building-blocks/hypercore) where write access is gated by a quorum of signing keys instead of a single owner machine. This is what production Pear apps use so no single laptop can push a malicious update. The setup, in 30 seconds: 1. Every signer generates a signing key: `pear multisig keys get`. Each signer's public key goes in a shared list. 2. One person sets the `multisig` object in `pear.json` with the signers' `publicKeys`, a `quorum` (for example, 2 of 3), and a `namespace`. The provision link is not stored here—it is supplied as the source when you prepare and commit each request. 3. `pear multisig link` outputs the new `pear://` link, derived from the namespace, public keys, and quorum. Set this as your `upgrade` field. The release flow becomes: ```bash pear multisig request # prepare a signing request pear multisig sign # each signer runs this; shares response pear multisig verify [...responses] pear multisig commit [...responses] # commits when quorum is reached ``` [Release pipeline](/explanation/deployment-releasing-apps-p2p) and [Deploy a Pear desktop app](/how-to/operate-an-app/manual-deployment/deployment) cover the full quorum lifecycle, key rotation, recovering from lost write-access, and the [release lines](/explanation/deployment-releasing-apps-p2p#release-lines) pattern (development → staging → rc → prerelease → production) that real teams use.
What you've learned [#what-youve-learned] You now have an end-to-end mental model for iterating a Pear Electron app: | Stage | What it is | Reversible? | | ------------------ | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | Iterate loop | `npm version patch` → make → `pear build` → `pear stage` → `pear provision` | Yes—reprovision from a different stage length | | OTA cycle | `pear.updater` emits `updating`/`updated`; renderer button calls `applyUpdate` + `appAfterUpdate` | Yes—restart loads the previous bundle until the next swap | | Stage vs provision | Stage link for append-only sync; provision link for what peers poll | History is permanent on the stage core | | Multisig commit | Quorum signs and publishes | Cryptographically committed | Every release iteration after part 3 is the same pattern: `npm version patch`, `npm run make` (on each OS you ship), `pear build`, `pear stage --dry-run`, `pear stage`, `pear provision`. Once multisig is wired, the four-step multisig flow replaces publishing directly to the provision link for production. Where to go next [#where-to-go-next] * [Deploy a Pear desktop app](/how-to/operate-an-app/manual-deployment/deployment)—the canonical how-to with every command, every flag, and every recovery procedure. * [Build desktop distributables](/how-to/operate-an-app/build-and-package/build-desktop-distributables)—code-signing, notarization, MSIX publisher details. * [Troubleshoot desktop releases](/how-to/operate-an-app/manual-deployment/troubleshoot-desktop-releases)—"the app did not update," lost write-access, stage size blowups, OTA polling cadence. * [Release pipeline](/explanation/deployment-releasing-apps-p2p)—the conceptual picture, deployment layers, and release lines. * [Release pipeline glossary](/explanation/deployment-releasing-apps-p2p#glossary)—terminology. * [`hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron)—the upstream template every snippet in this getting started path is based on. # Start from a template import { Cards, Card } from 'fumadocs-ui/components/card' The fastest way to a production-shaped app is to clone an official boilerplate and learn where each piece lives. Both embed [`pear-runtime`](/reference/pear/runtime) for peer-to-peer over-the-air updates—pick the one that matches what you're shipping. Which one? [#which-one] * **Building a desktop GUI?** Start from [hello-pear-electron](/getting-started/from-a-template/start-from-hello-pear-electron)—it ships a renderer and the preload bridge that connects your UI to peer-to-peer logic in a Bare worker. * **Building a CLI, daemon, or other headless tool?** Start from [hello-pear-bare](/getting-started/from-a-template/start-from-hello-pear-bare)—it compiles to a single standalone binary per OS and architecture. Both share the same peer-to-peer core: the [Bare](/reference/modules/bare-modules) runtime, [`pear-runtime`](/reference/pear/runtime) for updates, and the [Hyperswarm](/reference/building-blocks/hyperswarm) + [Corestore](/reference/helpers/corestore) stack. Your peer-to-peer logic is portable between them—only the shell (GUI versus terminal) changes. See [Runtime and languages](/explanation/runtime-and-languages). Once you've cloned a template and made it yours, ship it over the air: [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment) is the step-by-step release flow, and [Release pipeline](/explanation/deployment-releasing-apps-p2p) explains the stage → provision → multisig model behind it. Where to go next [#where-to-go-next] * [Build a peer-to-peer chat](/getting-started/build-a-peer-to-peer-chat/build-a-peer-to-peer-chat)—build the desktop app up from scratch instead of cloning. * [Pear desktop application architecture](/explanation/pear-desktop-architecture)—the renderer/main/worker model behind the Electron template. * [Runtime and languages](/explanation/runtime-and-languages)—where Bare and the runtimes fit across desktop, terminal, and mobile. # Start from the hello-pear-bare template This is the terminal counterpart to [Start from the hello-pear-electron template](/getting-started/from-a-template/start-from-hello-pear-electron). Instead of a desktop window, you start from a finished command-line boilerplate and learn how a Bare app wires peer-to-peer updates and replication through a worker—then ship it as a standalone binary. [`holepunchto/hello-pear-bare`](https://github.com/holepunchto/hello-pear-bare) is Holepunch's official boilerplate for peer-to-peer terminal applications. Where the Electron template embeds [`pear-runtime`](/reference/pear/runtime) into Electron, this template embeds it into [Bare](/reference/modules/bare-modules), the zero-core embeddable JavaScript runtime. The application and runtime compile into a single standalone executable per OS and architecture with no peer dependencies—no Node.js, and no Bare or [Pear CLI](/reference/pear/cli) required on the user's machine. With this shape you can build CLIs, REPLs, TUIs, services and daemons, or transport hooks—anything headless—and ship it with peer-to-peer over-the-air (OTA) updates. The Pear CLI v3 is built on this same architecture: a Bare standalone executable that updates itself peer-to-peer through `pear-runtime`. {/* Shared snippet (content/_snippets/) included via Fumadocs by getting-started pages that introduce peer-to-peer, pointing newcomers at the two core explanations. See: https://www.fumadocs.dev/docs/markdown#include */} New to peer-to-peer? [Peer-to-peer, demystified](/explanation/peer-to-peer-demystified) explains how peers find each other and connect directly—no servers—and [How Pear and Bare fit together](/explanation/pear-and-bare) maps the pieces you'll wire together. Take this path when you want to distribute a terminal app as a single executable and update it over the air. If you want to build a desktop app instead, follow the [hello-pear-electron template](/getting-started/from-a-template/start-from-hello-pear-electron). {/* Shared snippet (content/_snippets/) included via Fumadocs by guides that run `pear` CLI commands, so readers can install the CLI before following the steps. See: https://www.fumadocs.dev/docs/markdown#include */} **Need the `pear` CLI?** Install it from **[install.pears.com](https://install.pears.com)**, or prefix any command below with `npx`. See [Install & upgrade](/reference/pear/cli#install) for details. 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-bare cd hello-pear-bare npm install ``` 2. Create a valid upgrade link [#2-create-a-valid-upgrade-link] The template ships with a placeholder `upgrade` link in `package.json`. Until you replace it, startup fails with `INVALID_URL`. Create a real link with [`pear touch`](/reference/pear/cli#pear-touch-flags): ```bash pear touch ``` This prints a link, for example: `pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o`. 3. Set the upgrade link in package.json [#3-set-the-upgrade-link-in-packagejson] Set the `upgrade` field in `package.json` to the link you just created: ```json "upgrade": "pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o" ``` 4. Run the app [#4-run-the-app] `npm start` runs `bare bin.mjs --no-updates`: the process starts in development mode with over-the-air updates disabled, so a live release never swaps the binary while you work. ```bash npm start ``` To exercise the updater locally, opt back in: ```bash npm start -- --updates ``` Map the template [#map-the-template] | Path | What it is | Do you edit it? | | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | | [`bin.mjs`](https://github.com/holepunchto/hello-pear-bare/blob/main/bin.mjs) | The entrypoint—parses CLI flags, resolves the storage path, constructs the `App`, and logs its updater events. | Yes—your startup and CLI. | | [`app.js`](https://github.com/holepunchto/hello-pear-bare/blob/main/app.js) | The `App` class (a [`ready-resource`](https://github.com/holepunchto/ready-resource)). Spawns the Bare worker with [`PearRuntime.run`](/reference/pear/runtime#running-workers), wraps its IPC in a [`FramedStream`](https://www.npmjs.com/package/framed-stream), and turns updater messages into events. | Sometimes—app lifecycle. | | [`workers/main.js`](https://github.com/holepunchto/hello-pear-bare/blob/main/workers/main.js) | The Bare [worker](/explanation/workers) that owns the peer-to-peer code and the [`pear-runtime`](/reference/pear/runtime) updater. | Yes—this is your backend. | | [`package.json`](https://github.com/holepunchto/hello-pear-bare/blob/main/package.json) | App metadata, scripts, the `upgrade` link, and the per-platform `make:*` build targets. | Yes—branding and release link. | | [`scripts/make.js`](https://github.com/holepunchto/hello-pear-bare/blob/main/scripts/make.js) | Detects the host OS and architecture and runs the matching `make:` target. | Rarely. | | [`test/index.js`](https://github.com/holepunchto/hello-pear-bare/blob/main/test/index.js) | The [`brittle`](https://github.com/holepunchto/brittle) test entry, run by `npm test`. | Yes—your tests. | Like the Electron template, the peer-to-peer logic lives in a Bare [worker](/explanation/workers); `bin.mjs` and `app.js` are the host that spawns it and drives updates. There's no renderer or preload bridge—your "frontend" is the terminal. Upstream ships the worker as the [`hello-pear-worker`](https://github.com/holepunchto/hello-pear-worker) package, so the template's `workers/main.js` is just `require('hello-pear-worker')`. That is where your peer-to-peer code goes—open [Corestore](/reference/helpers/corestore) cores, join [Hyperswarm](/reference/building-blocks/hyperswarm) topics, and run your protocol. How it's wired [#how-its-wired] The template is three layers: `bin.mjs` (entry + CLI), `app.js` (the host that runs the worker and updater), and the Bare [worker](/explanation/workers) that holds your peer-to-peer code. `bin.mjs` is the Bare entry process. It parses three flags with [`paparam`](https://github.com/holepunchto/paparam)—`--version`, `--storage`, and `--no-updates`—then constructs the `App`: ```js file=/examples/getting-started/hello-pear-bare/bin.mjs#L13-L19 title="bin.mjs" const cmd = command( appName, summary(pkg.description), flag('--version|-v', 'Print the current version'), flag('--storage ', 'custom storage directory'), flag('--no-updates', 'disable OTA updates for this run') ) ``` It resolves a storage directory, then hands the runtime configuration to a `new App(...)`. In development (launched with `bare`) storage is a temporary directory; a packaged binary uses the persistent per-app directory from [`bare-storage`](/reference/modules/bare-modules), and `--storage` overrides both—see [Storage and distribution](/explanation/storage-and-distribution): ```js file=/examples/getting-started/hello-pear-bare/bin.mjs#L28-L41 title="bin.mjs" const updates = cmd.flags.updates const storage = cmd.flags.storage || (isDev ? null : path.join(persistent(), appName)) const dir = storage || path.join(os.tmpdir(), 'pear', appName) console.log(`Updates: ${updates === false ? 'disabled' : 'enabled'}`) const app = new App({ dir, app: isDev ? null : os.execPath(), updates, version: pkg.version, upgrade: pkg.upgrade, name: isWindows ? appName + '.exe' : appName }) ``` `App` (in `app.js`) is a [`ready-resource`](https://github.com/holepunchto/ready-resource). When it opens, it spawns the Bare worker with [`PearRuntime.run`](/reference/pear/runtime#running-workers)—passing the runtime config as arguments—and wraps the worker's IPC pipe in a [`FramedStream`](https://www.npmjs.com/package/framed-stream): ```js file=/examples/getting-started/hello-pear-bare/app.js#L20-L31 title="app.js" _open() { this.IPC = PearRuntime.run(require.resolve('./workers/main.js'), [ String(this.updates), this.version, this.upgrade, this.name, this.dir, this.app || '' ]) this.pipe = new FramedStream(this.IPC) this.pipe.on('data', (data) => this._onmessage(data)) ``` The worker owns the peer-to-peer code and the [`pear-runtime`](/reference/pear/runtime) updater. It messages the host, and `App` turns those into `updating`, `updated`, and `update-applied` events—which `bin.mjs` logs—applying each downloaded release automatically. `SIGHUP`, `SIGINT`, `SIGQUIT`, and `SIGTERM` handlers call `app.exit()` to close cleanly. For the model behind these updates, see [Pear OTA](/reference/pear/runtime#updates): ```js file=/examples/getting-started/hello-pear-bare/bin.mjs#L43-L55 title="bin.mjs" app.on('message', (message) => console.log(message)) app.on('updating', () => console.log('[updater] getting new update')) app.on('updating-delta', (delta) => console.log('[updater]', delta)) app.on('updated', () => console.log('[updater] update complete... applying')) app.on('update-applied', () => console.log('[updater] applied update, restart to run latest version') ) app.on('error', (err) => console.error('[app:error]', err)) process.on('SIGHUP', () => app.exit(129)) process.on('SIGINT', () => app.exit(130)) process.on('SIGQUIT', () => app.exit(131)) process.on('SIGTERM', () => app.exit(143)) ``` Variants [#variants] The `main` branch above (a Bare worker thread) is one of three process shapes `holepunchto/hello-pear-bare` ships. All three share the same `upgrade` link, the same per-platform build targets, and the same [`pear install`](/reference/pear/cli#pear-install)/[`pear seed`](/explanation/availability-and-blind-peering#seeding-with-pear-seed) release flow. What differs is `app.js`, `bin.mjs`, and their dependencies—and **neither variant has a `workers/` directory at all**, so the `workers/main.js` row in [Map the template](#map-the-template) applies to `main` only; on both variants the peer-to-peer code and the updater live in the same process tree as the CLI. Pick the branch that matches how your CLI runs: | Branch | Shape | Use it for | | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | [`main`](https://github.com/holepunchto/hello-pear-bare) (this page) | `pear-runtime` inside a Bare [worker](/explanation/workers) thread | Long-lived programs (services, REPLs, TUIs) that keep peer-to-peer logic off the main thread. | | [`variant/single-thread`](https://github.com/holepunchto/hello-pear-bare/tree/variant/single-thread) | `pear-runtime` constructed directly in the main Bare process—no worker, no IPC framing | Long-lived programs whose peer-to-peer logic doesn't need a separate thread. | | [`variant/daemon`](https://github.com/holepunchto/hello-pear-bare/tree/variant/daemon) | `pear-runtime` runs inside a detached [`bare-daemon`](https://github.com/holepunchto/bare-daemon) process that the foreground command spawns and returns from immediately | Short-lived CLI invocations (like `git`) that shouldn't block on an update check—the command exits while the daemon updates in the background. | Clone whichever branch fits, swapping it into step 1 of [Clone and run](#clone-and-run): ```bash git clone -b variant/single-thread https://github.com/holepunchto/hello-pear-bare ``` single-thread [#single-thread] Same `App` [`ready-resource`](https://github.com/holepunchto/ready-resource) shape as `main`, emitting the `updating`, `updated`, `update-applied`, and `error` events `bin.mjs` logs—plus `updating-delta`, which carries download progress and which `main`'s worker never actually forwards. (`main` also emits a generic `message` event for anything else the worker writes over the pipe; with no worker, single-thread has no equivalent.) The difference is in `_open()`, which constructs `PearRuntime` directly instead of spawning a worker and wrapping its IPC in a `FramedStream`: ```js file=/examples/getting-started/hello-pear-bare-single-thread/app.js#L23-L41 title="app.js" _open() { const store = new Corestore(path.join(this.dir, 'pear-runtime', 'corestore')) const swarm = new Hyperswarm() this.store = store this.swarm = swarm const pear = new PearRuntime({ dir: this.dir, app: this.app, updates: this.updates, version: this.version, upgrade: this.upgrade, name: this.name, store, swarm }) this.pear = pear ``` See [`app.js`](https://github.com/holepunchto/hello-pear-bare/blob/variant/single-thread/app.js) on the `variant/single-thread` branch for the full file. daemon [#daemon] `bin.mjs` spawns a **detached** updater and returns immediately—the foreground command never blocks on the network: ```js file=/examples/getting-started/hello-pear-bare-daemon/bin.mjs#L50-L57 title="bin.mjs" if (updates !== false) { try { App.spawnUpdater(dir, os.execPath(), isDev ? Bare.argv[1] : null, wait) } catch (err) { console.error('[app:error]', err) Bare.exit(1) } } ``` `App.spawnUpdater` re-invokes the same executable with a hidden `--updater` flag through [`bare-daemon`](https://github.com/holepunchto/bare-daemon): ```js file=/examples/getting-started/hello-pear-bare-daemon/app.js#L11-L18 title="app.js" static spawnUpdater(dir, app, entrypoint, updateWindow) { const args = entrypoint === null ? [] : [entrypoint] args.push('--updater', '--storage', dir) if (updateWindow !== undefined) { args.push('--update-window', String(updateWindow)) } return daemon.spawn(app, args) } ``` The daemon acquires an `updater.lock` file in the storage directory—via [`fs-native-extensions`](https://github.com/holepunchto/fs-native-extensions)—so only one updater runs per storage directory at a time, and logs to `/updates.log` instead of stdout, since nothing is attached to read it. `--update-window` (default `30000`, in milliseconds) bounds only how long the daemon waits for a download to **start**. Once one starts, it stays alive until the update is applied or errors out, however long that takes. See [`app.js`](https://github.com/holepunchto/hello-pear-bare/blob/variant/daemon/app.js) and [`bin.mjs`](https://github.com/holepunchto/hello-pear-bare/blob/variant/daemon/bin.mjs) on the `variant/daemon` branch for the full files. Give a run longer to pick up a download before the daemon gives up: ```bash npm start -- --updates --update-window 60000 ``` Two steps from [Clone and run](#clone-and-run) behave differently on this branch. `npm start` **returns immediately** rather than staying up, so there is no `Ctrl+C` to press. And because the foreground process never constructs [`pear-runtime`](/reference/pear/runtime), an unreplaced placeholder `upgrade` link does **not** fail with `INVALID_URL` in your terminal—the detached updater writes that error to `/updates.log`. Check that file first whenever updates appear not to run. Build a standalone binary [#build-a-standalone-binary] [`bare-build`](https://github.com/holepunchto/bare-build) compiles `bin.mjs` and its dependencies into a single standalone executable with no peer dependencies—users don't need Node.js, Bare, or the [Pear CLI](/reference/pear/cli) installed. Build for your current host with: ```bash npm run make ``` `scripts/make.js` detects your OS and architecture and runs the matching target, writing the binary to `out/-`. To build for a specific target—for example, in CI—call that target directly: ```bash npm run make:darwin-arm64 ``` The template ships a target for every supported platform: * **macOS**: `darwin-arm64`, `darwin-x64` * **Linux**: `linux-arm64`, `linux-x64` * **Windows**: `win32-arm64`, `win32-x64` Build each platform's binary on a matching host. Install over the air [#install-over-the-air] Distribute the executable through the usual channels—a download on your website, `apt`, or Homebrew. You can also install it peer-to-peer: once your `upgrade` link is [seeding](/explanation/availability-and-blind-peering#seeding-with-pear-seed) a release, pull it directly with the [`pear install`](/reference/pear/cli#pear-install) command: ```bash pear install pear:// ``` After the first install, new releases reach users through the swarm—no reinstall needed. For how staging and seeding work, see [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment) and [Installing applications](/explanation/storage-and-distribution#installing-applications). Example: add a flag [#example-add-a-flag] The boilerplate is a minimal CLI plus updater; `bin.mjs` holds the CLI and startup logic (your peer-to-peer code lives in the [worker](/explanation/workers)). Flags are parsed with `paparam`, so adding one is a two-line change. Add a `--name` flag to the command definition: ```diff const cmd = command( appName, summary(pkg.description), flag('--version|-v', 'Print the current version'), flag('--storage ', 'custom storage directory'), - flag('--no-updates', 'disable OTA updates for this run') + flag('--no-updates', 'disable OTA updates for this run'), + flag('--name ', 'who to greet on startup') ) ``` Then log the greeting after the other startup logs: ```diff console.log(`Updates: ${updates === false ? 'disabled' : 'enabled'}`) + +if (cmd.flags.name) console.log(`Hello, ${cmd.flags.name}!`) ``` Run `npm start -- --name YourName`, and the process greets you on boot. That is the CLI layer in `bin.mjs`; your real peer-to-peer features go in the [worker](/explanation/workers) (`workers/main.js`): * open a [Corestore](/reference/helpers/corestore) core, * join a [Hyperswarm](/reference/building-blocks/hyperswarm) topic, and * run your protocol. Reuse the storage directory the host passes to the worker so your data lands alongside the updater's—see [Storage and distribution](/explanation/storage-and-distribution). Customize for your brand [#customize-for-your-brand] Before you ship, set your identity in `package.json`: * `name`, `productName`, `description`, `author`, and `license`. * the `upgrade` `pear://` link (see [Create a valid upgrade link](#2-create-a-valid-upgrade-link)). `bin.mjs` reads `productName || name` for both the binary name and the persistent storage directory, so set these before your first release. Then build with `npm run make` and release with [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment). Where to go next [#where-to-go-next] * [Start from a template](/getting-started/from-a-template)—both boilerplates, side by side. * [Start from the hello-pear-electron template](/getting-started/from-a-template/start-from-hello-pear-electron)—the desktop counterpart, with a renderer, preload bridge, and Bare worker. * [Inside Bare](/explanation/bare-runtime)—what the runtime this template builds on actually is, and its lifecycle. * [How Pear and Bare fit together](/explanation/pear-and-bare)—where Bare and Pear sit relative to each other. * [Runtime and languages](/explanation/runtime-and-languages)—where Bare fits among Pear's runtimes. * [Bundle a Bare app](/how-to/run-on-native/bundle-a-bare-app)—the `bare-pack` and `bare-build` paths behind `npm run make`. * [Bare modules](/reference/modules/bare-modules)—the Bare standard library this template builds on. * [Pear OTA](/reference/pear/runtime)—the `pear-runtime` API behind the updater. * [Migrate from pear run to Pear OTA](/how-to/operate-an-app/migration)—if you're moving an existing app onto `pear-runtime`. * [Publish with GitHub Actions](/how-to/operate-an-app/github-actions/publish-with-github-actions)—stage a stable `pear://` link on every push with the `pear-ci` action. * [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment)—the full-control manual path for staging, provisioning, and release lines. * [Seeding with `pear seed`](/explanation/availability-and-blind-peering#seeding-with-pear-seed)—keep your `pear://` link online so peers can fetch releases. # Start from the hello-pear-electron template This is an alternative to the [four-part getting started path](/getting-started). Instead of building a chat from five files and reshaping it into the production template, you start from the finished template and learn your way around it. [`holepunchto/hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron) is Holepunch's official Electron template—the same shape [Keet](https://keet.io) and [PearPass](https://pass.pears.com) ship. This is a clone-first tour: where to put your UI, where to put your peer-to-peer logic, and how the two halves talk. For the conceptual "why" behind the split, read [Pear desktop application architecture](/explanation/pear-desktop-architecture). {/* Shared snippet (content/_snippets/) included via Fumadocs by getting-started pages that introduce peer-to-peer, pointing newcomers at the two core explanations. See: https://www.fumadocs.dev/docs/markdown#include */} New to peer-to-peer? [Peer-to-peer, demystified](/explanation/peer-to-peer-demystified) explains how peers find each other and connect directly—no servers—and [How Pear and Bare fit together](/explanation/pear-and-bare) maps the pieces you'll wire together. Take this path when you want a production-shaped Electron app from the first commit. If you would rather learn the moving parts by building up from scratch, follow the [four-part path](/getting-started) instead. {/* Shared snippet (content/_snippets/) included via Fumadocs by guides that run `pear` CLI commands, so readers can install the CLI before following the steps. See: https://www.fumadocs.dev/docs/markdown#include */} **Need the `pear` CLI?** Install it from **[install.pears.com](https://install.pears.com)**, or prefix any command below with `npx`. See [Install & upgrade](/reference/pear/cli#install) for details. 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-electron cd hello-pear-electron npm install ``` 2. Create a valid upgrade link [#2-create-a-valid-upgrade-link] Before the app will boot, set a valid `upgrade` link. To do this, run the following command: ```bash pear touch ``` This will output a link, for example: `pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o`. 3. Set the upgrade link in package.json [#3-set-the-upgrade-link-in-packagejson] Set the `upgrade` link in `package.json` to the link you just created: ```json "upgrade": "pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o" ``` 4. Run the app [#4-run-the-app] `npm start` runs `electron-forge start -- --no-updates`: the app launches in development mode with over-the-air updates disabled, so a live release never replaces your working tree while you hack. ```bash npm start ``` Map the template [#map-the-template] | Path | What it is | Do you edit it? | | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------ | | [`renderer/`](https://github.com/holepunchto/hello-pear-electron/tree/main/renderer) | The frontend—`index.html` plus `app.js`, plain DOM with no bundler. | Yes—this is your UI. | | [`workers/main.js`](https://github.com/holepunchto/hello-pear-electron/blob/main/workers/main.js) | The app logic—a [Bare](/reference/modules/bare-modules) worker that owns the swarm, storage, and updater. | Yes—this is your backend. | | [`electron/main.js`](https://github.com/holepunchto/hello-pear-electron/blob/main/electron/main.js) | The Electron main process. Spawns the worker and proxies messages; you rarely change it. | Occasionally. | | [`electron/preload.js`](https://github.com/holepunchto/hello-pear-electron/blob/main/electron/preload.js) | Exposes the safe `window.bridge` API to the renderer. | Only to add typed methods. | | [`package.json`](https://github.com/holepunchto/hello-pear-electron/blob/main/package.json) | App metadata, scripts, and the `upgrade` link. | Yes—branding and release link. | | [`forge.config.js`](https://github.com/holepunchto/hello-pear-electron/blob/main/forge.config.js) | Electron Forge packagers, makers, and signing hooks. | For packaging and signing. | | [`build/`](https://github.com/holepunchto/hello-pear-electron/tree/main/build) | Icons and per-OS manifests (`AppxManifest.xml`, entitlements, Flatpak/Snap metadata). | Yes—brand assets. | | [`pear.json`](https://github.com/holepunchto/hello-pear-electron/blob/main/pear.json) | Multisig config placeholder for production releases. | At production time. | The three pieces you care about day to day are: * `renderer/` (view) * `workers/main.js` (logic) * the bridge that connects them How the three processes connect [#how-the-three-processes-connect] The renderer never touches `ipcRenderer` directly—it only calls `window.bridge`. The main process is a thin proxy: it relays bridge calls to a [FramedStream](https://www.npmjs.com/package/framed-stream) byte stream connected to the Bare worker, and fans the worker's output back out to the renderer on per-worker channels named after the worker specifier (`/workers/main.js`). The boilerplate ships a "hello" round-trip you can trace on boot: 1. The renderer calls `bridge.startWorker('/workers/main.js')` (`renderer/app.js`). 2. The main process handles `pear:startWorker` and launches the worker with `PearRuntime.run()` ([see below](#where-the-app-logic-goes) for more details). 3. The worker runs `pipe.write('Hello from worker')` ([see below](#where-the-app-logic-goes) for more details). 4. The main process forwards it on `pear:worker:ipc:/workers/main.js`. 5. The renderer's `bridge.onWorkerIPC` callback receives it and replies `bridge.writeWorkerIPC('/workers/main.js', 'Hello from renderer')` ([see below](#how-to-connect-them) for more details). Where the frontend goes [#where-the-frontend-goes] Your UI lives in `renderer/`. `index.html` is a static shell loaded by the main process; `renderer/app.js` is loaded as an ES module and drives the DOM: ```js file=/examples/getting-started/hello-pear-electron/renderer/app.js#L1-L4 title="renderer/app.js" const bridge = window.bridge const decoder = new TextDecoder('utf-8') document.getElementById('v').innerText += bridge.pkg().version ``` There is no framework or build step—bring your own (React, Vue, plain DOM) by editing these files. The only rule is: the renderer talks to the rest of the app **only** through `window.bridge`. It has no Node or Bare access of its own, which is what keeps the renderer sandboxed. If you prefer to develop the UI against a dev server (hot reload, a framework toolchain), set `PEAR_DEV_SERVER_URL` and the main process loads that URL instead of `renderer/index.html`. The full production bridge—including how `window.bridge` is assembled—is walked through in [Reshape your app for production](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app). 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`](/reference/pear/runtime) instance: ```js file=/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](/reference/helpers/corestore) cores, join [Hyperswarm](/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](/explanation/storage-and-distribution). For why the logic belongs here rather than in the renderer, see [Workers](/explanation/workers). 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. How to connect them [#how-to-connect-them] The bridge is the contract between the two halves. `electron/preload.js` exposes these methods on `window.bridge`: | `window.bridge` method | Purpose | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | [`pkg()`](https://github.com/holepunchto/hello-pear-electron/blob/main/electron/preload.js#L9-L11) | Read `package.json` synchronously (used for the version label). | | [`startWorker(specifier)`](https://github.com/holepunchto/hello-pear-electron/blob/main/electron/preload.js#L14) | Spawn a worker by path, for example `/workers/main.js`. | | [`writeWorkerIPC(specifier, data)`](https://github.com/holepunchto/hello-pear-electron/blob/main/electron/preload.js#L35-L37) | Send a message to that worker. | | [`onWorkerIPC(specifier, listener)`](https://github.com/holepunchto/hello-pear-electron/blob/main/electron/preload.js#L25-L29) | Receive messages from that worker. Returns an unsubscribe function. | | [`onWorkerStdout`](https://github.com/holepunchto/hello-pear-electron/blob/main/electron/preload.js#L15-L19) / [`onWorkerStderr`](https://github.com/holepunchto/hello-pear-electron/blob/main/electron/preload.js#L20-L24) / [`onWorkerExit`](https://github.com/holepunchto/hello-pear-electron/blob/main/electron/preload.js#L30-L34) | Observe worker output and lifecycle. | | [`applyUpdate()`](https://github.com/holepunchto/hello-pear-electron/blob/main/electron/preload.js#L12) / [`appAfterUpdate()`](https://github.com/holepunchto/hello-pear-electron/blob/main/electron/preload.js#L13) | Apply a downloaded OTA update and relaunch. | Messages over the worker pipe are raw bytes—the boilerplate sends plain UTF-8 strings (`'Hello from worker'`, `'updating'`, `'pear:applyUpdate'`). You have two ways to add functionality: 1. Stay on the generic channel. Send and receive your own messages with `writeWorkerIPC` / `onWorkerIPC`. Define a small message protocol (a `type` field is enough). This is the lightest option and is shown below. 2. Add a typed bridge method. For something first-class (not tied to a worker), add a method to `electron/preload.js` and a matching `ipcMain.handle(...)` in `electron/main.js`, the way `applyUpdate` is wired. Example: add a "ping" feature [#example-add-a-ping-feature] You can add a button that asks the worker for the current time and shows the reply. It exercises the full round-trip: renderer to worker and back. You can switch the pipe from bare strings to small JSON messages so several message types can coexist. This example uses the generic channel. 1. Add a button to the renderer UI [#1-add-a-button-to-the-renderer-ui] In `renderer/index.html`, add a button and an output line inside `.container`: ```diff

v

+ +

``` 2. Send and receive in the renderer [#2-send-and-receive-in-the-renderer] In `renderer/app.js`, wire the button to `writeWorkerIPC` and handle the reply in the existing `onWorkerIPC` callback. The boilerplate currently treats every worker message as an updater event string; route messages through `JSON.parse` and fall back to the old strings so the updater keeps working: ```diff const offWorkerIpc = bridge.onWorkerIPC(workers.main, (data) => { const message = decoder.decode(data) console.log('worker ipc', '[', workers.main, ']:', message) - onWorkerUpdaterEvent(message) + + let parsed + try { + parsed = JSON.parse(message) + } catch { + onWorkerUpdaterEvent(message) // 'updating' / 'updated' + parsed = null + } + + if (parsed?.type === 'pong') { + document.getElementById('pong').innerText = 'Worker time: ' + parsed.time + } if (!sentHello) { sentHello = true bridge.writeWorkerIPC(workers.main, 'Hello from renderer') } }) + +document.getElementById('ping-btn').onclick = () => { + bridge.writeWorkerIPC(workers.main, JSON.stringify({ type: 'ping' })) +} ``` 3. Handle it in the worker [#3-handle-it-in-the-worker] In `workers/main.js`, parse incoming messages and reply to `ping`. Keep the existing `pear:applyUpdate` handling: ```diff pipe.on('data', async (data) => { const message = data.toString() if (message === 'pear:applyUpdate') { await pear.ready() await pear.updater.applyUpdate() pipe.write('pear:updateApplied') - } else console.log(message) + return + } + + try { + const msg = JSON.parse(message) + if (msg.type === 'ping') { + pipe.write(JSON.stringify({ type: 'pong', time: new Date().toISOString() })) + } + } catch { + console.log(message) + } }) ``` Run `npm start`, click **Ping worker**, and the worker's timestamp appears in the renderer. You now have a request/response path you can grow into real features—swap the `pong` handler for a [Corestore](/reference/helpers/corestore) read, a [Hyperswarm](/reference/building-blocks/hyperswarm) lookup, or any protocol your app needs. Customize for your brand [#customize-for-your-brand] Before you ship, [deploy your application](/how-to/operate-an-app/manual-deployment/deployment): * `package.json`—set `name`, `productName`, `description`, `author`, `license`, and the `upgrade` `pear://` link (see [Set the upgrade link](/how-to/operate-an-app/manual-deployment/deployment#1-set-the-upgrade-link)). * `build/`—replace `icon.icns` / `icon.ico` / `icon.png` and the sized icons, and edit `build/AppxManifest.xml` (`DisplayName`, `Publisher`) for Windows. * `pear.json`—fill the multisig public-key placeholders when you set up production signing. Then build and release with [Build desktop distributables](/how-to/operate-an-app/build-and-package/build-desktop-distributables), or automate it with [Build and sign desktop apps in CI](/how-to/operate-an-app/github-actions/build-and-sign-in-ci). Where to go next [#where-to-go-next] * [Pear desktop application architecture](/explanation/pear-desktop-architecture)—the conceptual model behind the renderer/main/worker split. * [Workers](/explanation/workers)—why peer-to-peer logic lives in a Bare worker and where the boundary should sit. * [Reshape into a production app](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app)—the hello-pear-electron-shaped scaffold with an [Autobase](/reference/building-blocks/autobase)-backed room, blind-pairing invites, and a vanilla HTML renderer. * [Start from a template](/getting-started/from-a-template)—both boilerplates, side by side. * [Start from the hello-pear-bare template](/getting-started/from-a-template/start-from-hello-pear-bare)—the terminal counterpart: a standalone Bare CLI with OTA updates and no GUI. * [Publish with GitHub Actions](/how-to/operate-an-app/github-actions/publish-with-github-actions)—stage a stable `pear://` link on every push with the `pear-ci` action. * [Build and sign desktop apps in CI](/how-to/operate-an-app/github-actions/build-and-sign-in-ci)—build, sign, and notarize macOS, Windows, and Linux distributables on hosted runners. * [Seeding with `pear seed`](/explanation/availability-and-blind-peering#seeding-with-pear-seed)—keep your `pear://` link online so peers can fetch releases. * [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment)—stage, seed, and release your build. # Add blind peering to a chat app import { Steps, Step } from 'fumadocs-ui/components/steps' This guide shows you how to **layer [blind peering](/explanation/availability-and-blind-peering) on top of [`pear-chat`](https://github.com/holepunchto/pear-docs/tree/preview/examples/getting-started/pear-chat)** so the room stays reachable even when none of its writers are connected. The reference implementation is [`pear-chat-blind-peering`](https://github.com/holepunchto/pear-docs/tree/preview/examples/how-to/blind-peering/add-blind-peering-to-a-chat-app). {/* Shared snippet (content/_snippets/) included via Fumadocs by the chat-app delta how-tos. Reminds readers these guides teach portable Pear-end (worker) logic, not desktop-specific code. See: https://www.fumadocs.dev/docs/markdown#include */} **This guide is about the Pear-end, not the shell.** The code below lives in the Bare [worker](/explanation/workers)—the peer-to-peer logic, not the user interface. Because the Pear-end never imports DOM APIs and never assumes a UI framework, the same worker is portable across **desktop (Electron)**, **mobile (React Native via Bare iOS / Bare Android)**, and **terminal**. The example apps ship an Electron shell, but only the UI half changes per platform—the logic here stays the same. See [Runtime and languages](/explanation/runtime-and-languages) for the cross-platform model and current support. {/* Shared snippet (content/_snippets/) included via Fumadocs by the chat-app delta how-tos. Flags the guide as delta-only on top of the pear-chat scaffold built in the getting-started path. See: https://www.fumadocs.dev/docs/markdown#include */} This is a **delta-only** how-to. The shared Electron + PearRuntime + Bare worker scaffold—with plain-JSON messages over a [`framed-stream`](https://www.npmjs.com/package/framed-stream) pipe and a vanilla HTML renderer—is built step by step in [Reshape into a production app](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app)—read that first. * [Keep data available with blind peering](/how-to/blind-peering/keep-data-available-with-blind-peering)—the standalone `blind-peering` client this guide layers onto the chat scaffold. Before you begin [#before-you-begin] * A working clone of `pear-chat` (or your own app built from [Reshape into a production app](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app)). * Comfort with [Autobase](/reference/building-blocks/autobase) and [Hyperswarm](/reference/building-blocks/hyperswarm). What changes [#what-changes] | Layer | Change | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Dependencies | Add [`blind-peering`](https://www.npmjs.com/package/blind-peering). | | Worker entrypoint | Declare a `--blind-peer-key` flag in `workers/index.js` so the keys reach the worker. | | Worker | Instantiate a `BlindPeering` client against the swarm, point it at one or more blind-peer public keys, register the room's [Autobase](/reference/building-blocks/autobase), and tear it down before the room. | | Renderer | Optional: surface the configured blind-peer keys, or let users supply their own. | Everything else—`electron/`, `spec/`, the vanilla `renderer/`, the forge config, the build assets—stays as in the getting-started chat app. Steps [#steps] Add the dependency [#add-the-dependency] ```bash skip="desktop-gui" npm install blind-peering ``` Declare the --blind-peer-key flag [#declare-the---blind-peer-key-flag] The worker only sees the flags `workers/index.js` declares with [`paparam`](https://www.npmjs.com/package/paparam). Add `--blind-peer-key` to the `command(...)` block so the keys you pass on the command line reach `WorkerTask`—without this, the runtime bails with `UNKNOWN_FLAG: blind-peer-key`: ```diff title="workers/index.js" skip="snippet-illustration" const cmd = command('pear-chat-blind-peering', + flag('--blind-peer-key|-b ', 'Blind peer key').multiple(), flag('--invite|-i ', 'Room invite'), flag('--name|-n ', 'Your name'), flag('--reset', 'Reset') ) ``` `.multiple()` lets the flag repeat, so `cmd.flags.blindPeerKey` is an **array** of keys. `cmd.flags` is already passed to `WorkerTask`, which reads it as `opts.blindPeerKey` in the next step. Wire a BlindPeering into WorkerTask [#wire-a-blindpeering-into-workertask] In `workers/worker-task.js`, instantiate [`BlindPeering`](https://www.npmjs.com/package/blind-peering) against the swarm and a [Corestore](/reference/helpers/corestore) namespace, pointing it at the public keys of the blind peers you want to mirror to (L23–L25, reading the keys collected from `opts.blindPeerKey` at L15). Register the room's Autobase once it is open (L36). Close it **before** the room so its connections release cleanly (L55): ```js file=/examples/how-to/blind-peering/add-blind-peering-to-a-chat-app/workers/worker-task.js title="workers/worker-task.js" lineNumbers {1,15,23-25,36,55} skip="example-import" const BlindPeering = require('blind-peering') const Corestore = require('corestore') const debounce = require('debounceify') const Hyperswarm = require('hyperswarm') const ReadyResource = require('ready-resource') const ChatRoom = require('./chat-room') class WorkerTask extends ReadyResource { constructor (pipe, storage, opts = {}) { super() this.pipe = pipe this.storage = storage this.blindPeerKeys = opts.blindPeerKey || [] this.invite = opts.invite this.name = opts.name || `User ${Date.now()}` this.store = new Corestore(storage) this.swarm = new Hyperswarm() this.swarm.on('connection', (conn) => this.store.replicate(conn)) this.blindPeering = new BlindPeering(this.swarm.dht, this.store.namespace('blind-peering'), { keys: this.blindPeerKeys }) this.room = new ChatRoom(this.store, this.swarm, this.invite) this.debounceMessages = debounce(() => this._messages()) this.room.on('update', () => this.debounceMessages()) } async _open () { await this.store.ready() await this.room.ready() await this.blindPeering.addAutobase(this.room.base) this.pipe.on('data', async (data) => { let message try { message = JSON.parse(data) } catch { return } if (message.type === 'add-message') { await this.room.addMessage(message.text, { name: this.name, at: Date.now() }) } }) await this.debounceMessages() this.pipe.write(JSON.stringify({ type: 'invite', invite: await this.room.getInvite() })) } async _close () { await this.blindPeering.close() await this.room.close() await this.swarm.destroy() await this.store.close() } async _messages () { const messages = await this.room.getMessages() messages.sort((a, b) => a.info.at - b.info.at) this.pipe.write(JSON.stringify({ type: 'messages', messages })) } } module.exports = WorkerTask ``` The blind peers replicate and seed the room's encrypted [Autobase](/reference/building-blocks/autobase) without holding its read key. Point the client at blind-peer keys you run yourself (the [`blind-peer-cli`](https://github.com/holepunchto/blind-peer-cli) server prints `Listening at ` on startup) or shared ones. See [Availability and blind peering](/explanation/availability-and-blind-peering) for the threat model (a blind peer replicates encrypted data but never holds the room encryption key). Confirm the teardown order [#confirm-the-teardown-order] The order in `_close` matters (L54–L58): the room holds [`blind-pairing`](https://www.npmjs.com/package/blind-pairing) handles that depend on the swarm, and the blind-peering client holds its own connections to the blind peers. Always close the client first (L55), then the room, then the swarm, then the store (L56–L58). Run it [#run-it] You need three processes: 1. A **blind peer** that stays online to keep the room available, and 2. **two chat peers** (`user1` and `user2`) that can come and go. Open a terminal for each. 1. Start a blind peer [#1-start-a-blind-peer] Run one with [`blind-peer-cli`](https://github.com/holepunchto/blind-peer-cli) (or point at a shared one you trust). On startup it logs a `Listening at ` line—copy that key, you pass it to both users as `--blind-peer-key`. ```bash skip="blind-peer" npm install -g blind-peer-cli blind-peer # {"level":30, ... ,"msg":"Listening at es4n7ty45odd1udfqyi9xz58mrbheuhdnxgdufsn9gz6e5uhsqco"} ``` 2. Start user1 (creates the room) [#2-start-user1-creates-the-room] Pass the blind-peer key so the room's [Autobase](/reference/building-blocks/autobase) is mirrored. The app prints an `Invite:` line on stdout—copy it for `user2`: ```bash skip="desktop-gui" npm run build npm start -- --storage /tmp/bp-user1 --name user1 --blind-peer-key ``` 3. Start user2 (joins the room) [#3-start-user2-joins-the-room] Pass the invite from `user1` and the same blind-peer key: ```bash skip="desktop-gui" npm start -- --storage /tmp/bp-user2 --name user2 --invite --blind-peer-key ``` Both peers register the room's Autobase with the blind peer (you can repeat `--blind-peer-key` to use several). Now quit `user1`: `user2` still receives the existing history and stays in sync, because the blind peer keeps replicating the encrypted room even though neither writer is online. Restart `user1` and it catches up from the blind peer too—without ever handing it the room's read key. Where to go next [#where-to-go-next] * [Keep data available with blind peering](/how-to/blind-peering/keep-data-available-with-blind-peering)—the same `blind-peering` client on its own, without the Electron shell. * [Availability and blind peering](/explanation/availability-and-blind-peering)—why a third-party replicator does not need to be trusted. * [Add Keet identity to a chat app](/how-to/manage-identity/add-keet-identity-to-a-chat-app)—keep your relay's identity portable. * [Build desktop distributables](/how-to/operate-an-app/build-and-package/build-desktop-distributables)—package the relay app for macOS, Windows, and Linux. # Blind peering import { Cards, Card } from 'fumadocs-ui/components/card' Recipes for keeping peer-to-peer data reachable when the writers that created it are offline. # Keep data available with blind peering **This guide focuses on the Pear/Bare logic.** It shows the [`blind-peering`](https://www.npmjs.com/package/blind-peering) client on its own—no Electron, no UI. For the same capability wired into a full desktop app with a chat front end, see the worked example [`pear-chat-blind-peering`](https://github.com/holepunchto/pear-docs/tree/preview/examples/how-to/blind-peering/add-blind-peering-to-a-chat-app) and its walkthrough, [Add blind peering to a chat app](/how-to/blind-peering/add-blind-peering-to-a-chat-app). A peer-to-peer core is only available while a peer that has it is online. [Blind peering](/explanation/availability-and-blind-peering) closes that gap: a **blind peer** is an always-on server that replicates and seeds your [Hypercore](/reference/building-blocks/hypercore) or [Autobase](/reference/building-blocks/autobase) **without holding the read capability**—it stores and forwards encrypted blocks it cannot decrypt. Your data stays reachable even when none of its writers are connected. This is purely Pear-end logic: it lives in a [Bare](/reference/modules/bare-modules) worker (or any Bare/Node process) and never touches a UI. {/* Shared snippet (content/_snippets/) included via Fumadocs by how-tos whose Pear-end (worker) logic works with EITHER boilerplate (desktop or terminal). Carries the cross-platform portability point too, so it REPLACES _pear-end-portability-callout on these guides—one orientation callout, not two. For frontend-specific how-tos, use _frontend-template-callout.mdx instead. See: https://www.fumadocs.dev/docs/markdown#include */} **Pear-end logic—start from a boilerplate.** The code in this guide lives in the Bare [worker](/explanation/workers): peer-to-peer logic, no UI. The same worker runs unchanged on desktop, terminal, and mobile—only the shell differs (see [Runtime and languages](/explanation/runtime-and-languages)). Start from a boilerplate in [Start from a template](/getting-started/from-a-template)—desktop ([`hello-pear-electron`](/getting-started/from-a-template/start-from-hello-pear-electron)) or terminal ([`hello-pear-bare`](/getting-started/from-a-template/start-from-hello-pear-bare))—and add this capability on top. Two roles [#two-roles] | Role | What it runs | Holds read key? | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | | **Blind peer** | An always-on server with its own key pair, run with [`blind-peer-cli`](https://github.com/holepunchto/blind-peer-cli)—which provides the `blind-peer` command on top of the [`blind-peer`](https://github.com/holepunchto/blind-peer) library. Seeds whatever it is asked to. | No | | **Client** | Your app, using the `blind-peering` module to ask one or more blind peers (by public key) to keep specific cores available. | Yes | You point the client at the **public keys** of the blind peers you want to mirror to. Those can be blind peers you run yourself, or shared ones. Add the dependency [#add-the-dependency] ```sh skip="install-instruction" npm install blind-peering ``` Wire the client [#wire-the-client] Create the `BlindPeering` client against your swarm's [HyperDHT](/reference/building-blocks/hyperdht) instance ([`swarm.dht`](/reference/building-blocks/hyperswarm#swarmdht)) and a [Corestore](/reference/helpers/corestore) namespace, passing the blind peers' public keys as `keys`. Then register the cores or Autobases you want kept available: ```js skip="snippet-illustration" import Hyperswarm from 'hyperswarm' import Corestore from 'corestore' import BlindPeering from 'blind-peering' const swarm = new Hyperswarm() const store = new Corestore('./store') swarm.on('connection', (conn) => store.replicate(conn)) // Public keys of the blind peers to mirror to (z32 or hex). const blindPeerKeys = [/* 'a1b2c3…' */] const blinds = new BlindPeering(swarm.dht, store.namespace('blind-peering'), { keys: blindPeerKeys }) // Ask the blind peers to keep a single Hypercore available… await blinds.addCore(core) // …or a whole Autobase (all of its writer and view cores). await blinds.addAutobase(base) ``` `addCore` / `addAutobase` connect to the closest configured blind peers and request that they replicate and seed the given cores. The blind peer downloads the encrypted blocks and serves them to other peers on demand—without ever being able to read them. Tear it down [#tear-it-down] Close the client before the swarm and store so its connections release cleanly: ```js skip="snippet-illustration" await blinds.close() await swarm.destroy() await store.close() ``` Run your own blind peer [#run-your-own-blind-peer] To control availability yourself rather than relying on shared blind peers, run the [`blind-peer-cli`](https://github.com/holepunchto/blind-peer-cli) server (`npm i -g blind-peer-cli`, then `blind-peer`) on an always-on machine. It prints a public key—pass that key in the client's `keys` array. See [Availability and blind peering](/explanation/availability-and-blind-peering) for the full setup, including a systemd unit. See also [#see-also] * [Availability and blind peering](/explanation/availability-and-blind-peering)—the threat model: why a blind peer never needs to be trusted with your data. * [Add blind peering to a chat app](/how-to/blind-peering/add-blind-peering-to-a-chat-app)—the same client wired into a full desktop app with a UI. * [Work with many Hypercores using Corestore](/how-to/store-and-replicate/work-with-many-hypercores-using-corestore)—the store the client namespaces into. * [Autobase reference](/reference/building-blocks/autobase)—the multi-writer log `addAutobase` keeps available. # Connect to many peers by topic with Hyperswarm In [Connect two peers by key with HyperDHT](/how-to/connect-to-peers/connect-two-peers-by-key-with-hyperdht), two peers connected directly using the first peer's public key. Hyperswarm helps to discover peers swarming a common topic, and connect to as many of them as possible. This will become clearer in [Replicate and persist with Hypercore](/how-to/store-and-replicate/replicate-and-persist-with-hypercore), but it's the best way to distribute peer-to-peer data structures. The [Hyperswarm](/reference/building-blocks/hyperswarm) module provides a higher-level interface over the underlying [HyperDHT](/reference/building-blocks/hyperdht), abstracting away the mechanics of establishing and maintaining connections. Instead, 'join' topics, and the swarm discovers peers automatically. It also handles reconnection in the event of failures. In [Connect two peers by key with HyperDHT](/how-to/connect-to-peers/connect-two-peers-by-key-with-hyperdht), we needed to explicitly indicate which peer was the server and which was the client. By using Hyperswarm, we create two peers, have them join a common topic, and let the swarm deal with connections. {/* Shared snippet (content/_snippets/) included via Fumadocs by how-tos whose Pear-end (worker) logic works with EITHER boilerplate (desktop or terminal). Carries the cross-platform portability point too, so it REPLACES _pear-end-portability-callout on these guides—one orientation callout, not two. For frontend-specific how-tos, use _frontend-template-callout.mdx instead. See: https://www.fumadocs.dev/docs/markdown#include */} **Pear-end logic—start from a boilerplate.** The code in this guide lives in the Bare [worker](/explanation/workers): peer-to-peer logic, no UI. The same worker runs unchanged on desktop, terminal, and mobile—only the shell differs (see [Runtime and languages](/explanation/runtime-and-languages)). Start from a boilerplate in [Start from a template](/getting-started/from-a-template)—desktop ([`hello-pear-electron`](/getting-started/from-a-template/start-from-hello-pear-electron)) or terminal ([`hello-pear-bare`](/getting-started/from-a-template/start-from-hello-pear-bare))—and add this capability on top. Create the peer-app project [#create-the-peer-app-project] Create the `peer-app` project with the following commands: ```sh example=hyperswarm-chat step=setup mkdir peer-app cd peer-app npm init -y npm pkg set type="module" npm install hyperswarm hypercore-crypto b4a bare-process ``` Alter the peer-app/index.js file to the following. Create a single [Hyperswarm](/reference/building-blocks/hyperswarm) instance (L7). On each `connection` event, track the connection and log incoming data from that peer (L13–L20). Broadcast anything typed on stdin to every open connection (L23–L28). Then join a common topic as both client and server—reusing a topic passed on the command line, or generating a fresh one (L31–L32). The [`discovery.flushed()`](/reference/building-blocks/hyperswarm#await-discoveryflushed) promise resolves once the topic has been announced to the DHT, at which point the topic is logged for other peers to copy (L35–L37). ```javascript file=/examples/how-to/connect-to-peers/connect-to-many-peers-by-topic-with-hyperswarm/peer-app/index.js title="peer-app/index.js" lineNumbers {7,13-20,23-28,31-32,35-37} skip="example-import" import Hyperswarm from 'hyperswarm' import crypto from 'hypercore-crypto' import b4a from 'b4a' import process from 'bare-process' const swarm = new Hyperswarm() const name = b4a.toString(swarm.keyPair.publicKey, 'hex') process.once('SIGINT', () => swarm.destroy().then(() => process.exit(0))) // Keep track of all connections and console.log incoming data const conns = [] swarm.on('connection', conn => { const peer = b4a.toString(conn.remotePublicKey, 'hex') console.log('* got a connection from:', peer, '*') conns.push(conn) conn.once('close', () => conns.splice(conns.indexOf(conn), 1)) conn.on('data', data => console.log(`${peer}: ${data}`)) conn.on('error', e => console.log(`Connection error: ${e}`)) }) // Broadcast stdin to all connections process.stdin.on('data', d => { console.log(`${name}: ${d}`) for (const conn of conns) { conn.write(d) } }) // Join a common topic const topic = Bare.argv[2] ? b4a.from(Bare.argv[2], 'hex') : crypto.randomBytes(32) const discovery = swarm.join(topic, { client: true, server: true }) // The flushed promise will resolve when the topic has been fully announced to the DHT discovery.flushed().then(() => { console.log('joined topic:', b4a.toString(topic, 'hex')) }) ``` {/* @harness example=hyperswarm-chat step=copy from=examples/how-to/connect-to-peers/connect-to-many-peers-by-topic-with-hyperswarm/peer-app/index.js to=peer-app/index.js */} Run the swarm [#run-the-swarm] In one terminal, move one directory up and run `peer-app` with `bare`: ```sh example=hyperswarm-chat step=run process=peer1 expect="joined topic:" capture-topic="joined topic: ([0-9a-f]+)" timeout=45000 bare peer-app ``` This will display the topic. Copy/paste that topic into as many additional terminals as desired: ```sh example=hyperswarm-chat step=run process=peer2 cmd="bare peer-app ${topic}" expect="got a connection from:" timeout=45000 bare peer-app ``` Each peer will log information about the other connected peers. Start typing into any terminal, and it will be broadcast to all connected peers. **Use one [Hyperswarm](/reference/building-blocks/hyperswarm) instance per application.** A single swarm can join multiple topics and dedups peer connections shared between them. This speeds up connections by reducing [HyperDHT](/reference/building-blocks/hyperdht) records per topic and simplifies managing the maximum number of connections your app makes. Connections established here are ephemeral—once both peers go offline the conversation is lost. See also [#see-also] * [Replicate and persist with Hypercore](/how-to/store-and-replicate/replicate-and-persist-with-hypercore)—persist messages so a reader catches up after the writer disconnects. * [Peer-to-peer, demystified](/explanation/peer-to-peer-demystified)—how topic discovery and hole punching work under Hyperswarm. * [Connect two peers by key with HyperDHT](/how-to/connect-to-peers/connect-two-peers-by-key-with-hyperdht)—direct one-to-one connection by public key. * [Hyperswarm reference](/reference/building-blocks/hyperswarm)—full API for topic joins, connection events, and peer management. * [HyperDHT reference](/reference/building-blocks/hyperdht)—the lower-level DHT layer Hyperswarm is built on. # Connect two peers by key with HyperDHT [`HyperDHT`](/reference/building-blocks/hyperdht) helps clients connect to a server peer with a known public key. HyperDHT uses a series of holepunching techniques to establish direct connections between the peers, even if they're located on home networks with tricky NATs. In the HyperDHT, peers are identified by a public key, not by an IP address. The public key is looked up in a decentralized hash table, which maps the key to an IP address and port. This means users can connect to each other irrespective of their location, even if they move between different networks. HyperDHT's holepunching will fail if both the client peer and the server peer are on randomizing [NATs](https://en.wikipedia.org/wiki/Network_address_translation), in which case the connection must be relayed through a third peer. HyperDHT does not do any relaying by default. For example, Keet implements its relaying system wherein other call participants can serve as relays -- the more participants in the call, the stronger overall connectivity becomes. Use the HyperDHT to create a basic CLI chat app where a client peer connects to a server peer by public key. The example consists of two applications: [`client-app`](#create-the-client-app) and [`server-app`](#create-the-server-app). {/* Shared snippet (content/_snippets/) included via Fumadocs by how-tos whose Pear-end (worker) logic works with EITHER boilerplate (desktop or terminal). Carries the cross-platform portability point too, so it REPLACES _pear-end-portability-callout on these guides—one orientation callout, not two. For frontend-specific how-tos, use _frontend-template-callout.mdx instead. See: https://www.fumadocs.dev/docs/markdown#include */} **Pear-end logic—start from a boilerplate.** The code in this guide lives in the Bare [worker](/explanation/workers): peer-to-peer logic, no UI. The same worker runs unchanged on desktop, terminal, and mobile—only the shell differs (see [Runtime and languages](/explanation/runtime-and-languages)). Start from a boilerplate in [Start from a template](/getting-started/from-a-template)—desktop ([`hello-pear-electron`](/getting-started/from-a-template/start-from-hello-pear-electron)) or terminal ([`hello-pear-bare`](/getting-started/from-a-template/start-from-hello-pear-bare))—and add this capability on top. This guide is a terminal-only walkthrough. For the **desktop equivalent**—the same peer connection wired into an Electron + Bare worker shell—follow the [Getting Started path](/getting-started), which builds a `pear://` chat app on top of the [`hello-pear-electron` template](/getting-started/from-a-template/start-from-hello-pear-electron). Create the server app [#create-the-server-app] The `server-app` creates a key pair, starts a server listening on it, and logs the public key. Copy that key—the client uses it to connect. Create the server project [#create-the-server-project] Create the `server-app` project with the following commands: ```sh example=hyperdht-chat step=setup mkdir server-app cd server-app npm init -y npm pkg set type="module" npm install hyperdht b4a bare-process ``` Add the server logic [#add-the-server-logic] Alter `server-app/index.js` to the following. The server starts a DHT node (L5) and generates a key pair that serves as its identity in the DHT (L8), then derives a hex string `name` from the public key to print and share (L9). `dht.createServer` registers a connection handler (L11–L13) that logs each connecting peer, wires incoming data to the console, and forwards local stdin to the connection (L15–L19). Finally, [`server.listen(keyPair)`](/reference/building-blocks/hyperdht#await-serverlistenkeypair) announces the key on the DHT and logs the public key for the client to copy (L22–L24): ```javascript file=/examples/how-to/connect-to-peers/connect-two-peers-by-key-with-hyperdht/server-app/index.js title="server-app/index.js" lineNumbers {5,8-9,11-13,15-19,22-24} skip="example-import" import DHT from 'hyperdht' import b4a from 'b4a' import process from 'bare-process' const dht = new DHT() // This keypair is the peer identifier in the DHT const keyPair = DHT.keyPair() const name = b4a.toString(keyPair.publicKey, 'hex') const server = dht.createServer(conn => { const peer = b4a.toString(conn.remotePublicKey, 'hex') console.log('* got a connection from:', peer, '*') conn.on('data', data => console.log(`${peer}: ${data}`)) process.stdin.on('data', d => { console.log(`${name}: ${d}`) conn.write(d) }) }) server.listen(keyPair).then(() => { console.log('listening on:', name) }) // Unannounce the public key before exiting the process // (Not strictly required, but it helps avoid DHT pollution.) process.once('SIGINT', () => server.close().then(() => process.exit(0))) ``` {/* @harness example=hyperdht-chat step=copy from=examples/how-to/connect-to-peers/connect-two-peers-by-key-with-hyperdht/server-app/index.js to=server-app/index.js */} Run the server [#run-the-server] To run the `server-app`, move one directory up and run the following command: ```sh example=hyperdht-chat step=run process=server expect="listening on:" capture-key="listening on: ([0-9a-f]+)" bare server-app ``` Create the client app [#create-the-client-app] Create the client project [#create-the-client-project] In another terminal create the `client-app` project with the following commands: ```sh example=hyperdht-chat step=setup mkdir client-app cd client-app npm init -y npm pkg set type="module" npm install hyperdht b4a bare-process ``` Add the client logic [#add-the-client-logic] Alter `client-app/index.js` to the following. The client reads the server's public key from the command-line argument and fails fast if it is missing (L6–L7), then decodes it from hex (L10). It starts its own DHT node (L12–L13) and calls `dht.connect(publicKey)` to begin hole punching toward the server (L15). The `open` handler logs the connected peer once the link is established (L16–L19), the `data` handler prints incoming messages (L21–L24), and local stdin is written to the connection (L26–L29): ```javascript file=/examples/how-to/connect-to-peers/connect-two-peers-by-key-with-hyperdht/client-app/index.js title="client-app/index.js" lineNumbers {6-7,10,12-13,15,16-19,21-24,26-29} skip="example-import" import DHT from 'hyperdht' import b4a from 'b4a' import process from 'bare-process' const key = Bare.argv[2] if (!key) throw new Error('provide a key') console.log('Connecting to:', key) const publicKey = b4a.from(key, 'hex') const dht = new DHT() const name = b4a.toString(dht.defaultKeyPair.publicKey, 'hex') const conn = dht.connect(publicKey) conn.once('open', () => { const peer = b4a.toString(conn.remotePublicKey, 'hex') console.log('* got a connection from:', peer, '*') }) conn.on('data', data => { const peer = b4a.toString(conn.remotePublicKey, 'hex') console.log(`${peer}: ${data}`) }) process.stdin.on('data', d => { console.log(`${name}: ${d}`) conn.write(d) }) ``` {/* @harness example=hyperdht-chat step=copy from=examples/how-to/connect-to-peers/connect-two-peers-by-key-with-hyperdht/client-app/index.js to=client-app/index.js */} Run the chat client [#run-the-chat-client] To run the `client-app`, move one directory up and run the following command: ```sh example=hyperdht-chat step=run process=client cmd="bare client-app ${key}" expect="Connecting to:" timeout=15000 bare client-app ``` {/* @harness example=hyperdht-chat step=expect process=server contains="got a connection from:" timeout=30000 */} The `client-app` will spin up a client, and the public key copied earlier must be supplied as a command line argument for connecting to the server. The client process will log `* got a connection from:` into the console when it connects to the server. Once it's connected, try typing in both terminals. See also [#see-also] * [Connect to many peers by topic with Hyperswarm](/how-to/connect-to-peers/connect-to-many-peers-by-topic-with-hyperswarm)—discover peers by a shared topic instead of a known public key. * [Peer-to-peer, demystified](/explanation/peer-to-peer-demystified)—hole punching, public-key identity, and when to use HyperDHT vs [Hyperswarm](/reference/building-blocks/hyperswarm). * [HyperDHT reference](/reference/building-blocks/hyperdht)—full API for the DHT node, servers, and connections used in this guide. * [Secretstream](/reference/helpers/secretstream)—the Noise-encrypted stream layer wrapping every HyperDHT connection. * [Dependencies and network](/explanation/dependencies-and-network)—the IP-level information peers observe during a HyperDHT connection. # Host multiple rooms in one chat app import { Steps, Step } from 'fumadocs-ui/components/steps' This guide shows you how to **extend [`pear-chat`](https://github.com/holepunchto/pear-docs/tree/preview/examples/getting-started/pear-chat) from a single room to an account model that owns and joins many rooms**. The reference implementation is [`pear-chat-multi-rooms`](https://github.com/holepunchto/pear-docs/tree/preview/examples/how-to/connect-to-peers/multi-rooms). {/* Shared snippet (content/_snippets/) included via Fumadocs by the chat-app delta how-tos. Reminds readers these guides teach portable Pear-end (worker) logic, not desktop-specific code. See: https://www.fumadocs.dev/docs/markdown#include */} **This guide is about the Pear-end, not the shell.** The code below lives in the Bare [worker](/explanation/workers)—the peer-to-peer logic, not the user interface. Because the Pear-end never imports DOM APIs and never assumes a UI framework, the same worker is portable across **desktop (Electron)**, **mobile (React Native via Bare iOS / Bare Android)**, and **terminal**. The example apps ship an Electron shell, but only the UI half changes per platform—the logic here stays the same. See [Runtime and languages](/explanation/runtime-and-languages) for the cross-platform model and current support. This is a **delta-only** how-to. The shared scaffold is explained in the [Reshape into a production app](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app) tutorial—read it first. Before you begin [#before-you-begin] * A working clone of `pear-chat` (or your own app built from the getting-started path). * Familiarity with [Autobase](/reference/building-blocks/autobase)—each room is one Autobase. What changes [#what-changes] | Layer | Change | | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | Worker | Introduce a `ChatAccount` that holds a map of `ChatRoom`s keyed by a generated room id. | | Schema | Add a `room` collection persisting the rooms a user has joined (`{ id, name, invite, info }`). | | Transport | Add `add-room` and `join-room` message types, push a `rooms` event, and make the `messages` event and `add-message` command carry a `roomId`. | | Renderer | Add a left-rail room list with a "new room" button and an invite-paste input. | The Electron shell, the build/forge configuration, and the per-room ChatRoom code stay identical to the getting-started chat app. Steps [#steps] Replace room with a ChatAccount on WorkerTask [#replace-room-with-a-chataccount-on-workertask] In `workers/worker-task.js`, swap the single `ChatRoom` for a `ChatAccount` (L6). The worker keeps the same [Corestore](/reference/helpers/corestore) + [Hyperswarm](/reference/building-blocks/hyperswarm) setup (L17–L19), but delegates all room management to the account (L21) and forwards its `messages` event over the worker pipe as JSON tagged with the `roomId` (L24–L26). `_open` opens the account (L30–L31), then handles `add-room`, `join-room`, and `add-message` on the pipe (L44–L50). `_close` tears down account → swarm → store (L55–L59): ```js file=/examples/how-to/connect-to-peers/multi-rooms/workers/worker-task.js title="workers/worker-task.js" lineNumbers {6,17-19,21-26,30-31,44-50,55-59} skip="example-import" const Corestore = require('corestore') const debounce = require('debounceify') const Hyperswarm = require('hyperswarm') const ReadyResource = require('ready-resource') const ChatAccount = require('./chat-account') class WorkerTask extends ReadyResource { constructor (pipe, storage, opts = {}) { super() this.pipe = pipe this.storage = storage this.invite = opts.invite this.name = opts.name || `User ${Date.now()}` this.store = new Corestore(storage) this.swarm = new Hyperswarm() this.swarm.on('connection', (conn) => this.store.replicate(conn)) this.account = new ChatAccount(this.store, this.swarm) this.debounceRooms = debounce(() => this._rooms()) this.account.on('update', () => this.debounceRooms()) this.account.on('messages', (roomId, messages) => { this.pipe.write(JSON.stringify({ type: 'messages', roomId, messages })) }) } async _open () { await this.store.ready() await this.account.ready() if (this.invite) { await this.account.joinRoom(this.invite) } this.pipe.on('data', async (data) => { let message try { message = JSON.parse(data) } catch { return } if (message.type === 'add-room') { await this.account.addRoom(message.name, { at: Date.now() }) } else if (message.type === 'join-room') { await this.account.joinRoom(message.invite) } else if (message.type === 'add-message') { await this.account.addMessage(message.roomId, message.text, { name: this.name, at: Date.now() }) } }) await this.debounceRooms() } async _close () { await this.account.close() await this.swarm.destroy() await this.store.close() } async _rooms () { const rooms = Object.entries(this.account.rooms).map(([id, room]) => ({ id, name: room.name, invite: room.invite, info: room.info })) // A just-joined room has no `info` yet — it only arrives once its // ChatAccount 'update' syncs the room metadata from the peer who created // it (see joinRoom in chat-account.js). If _rooms() runs in that window, // `.info` is undefined and a bare `.at` access throws, taking the whole // worker down. rooms.sort((a, b) => (a.info?.at ?? 0) - (b.info?.at ?? 0)) this.pipe.write(JSON.stringify({ type: 'rooms', rooms })) } } module.exports = WorkerTask ``` Build ChatAccount [#build-chataccount] Create `workers/chat-account.js` modelled on `chat-room.js`. The account is itself an [Autobase](/reference/building-blocks/autobase)-backed HyperDB that stores the user's room list: * each entry is `{ id, name, invite, info }`, where `id` is a generated handle and `invite` is the room's pairing code. * On `_open`, the account opens its base, then `openRooms()` materialises one `ChatRoom` per stored entry, each in its own Corestore namespace (`this.store.namespace(id)`) (L156–L168). * `addRoom` (L170–L184) and `joinRoom` (L186–L206) spin up a new `ChatRoom`, then append the room metadata to the account base so it survives restarts: ```js file=/examples/how-to/connect-to-peers/multi-rooms/workers/chat-account.js#L156-L206 title="workers/chat-account.js" lineNumbers {156-168,170-184,186-206} skip="example-import" async openRooms () { const rooms = await this.view.find('@pear-chat-multi-rooms/rooms', { reverse: true, limit: 100 }).toArray() await Promise.all(rooms.map(async (item) => { const roomStore = this.store.namespace(item.id) const room = new ChatRoom(roomStore, this.swarm, { name: item.name, info: item.info, invite: item.invite }) this.rooms[item.id] = room this._watchMessages(item.id) await room.ready() await this._messages(item.id) })) } async addRoom (name, info) { const id = Math.random().toString(16).slice(2) const roomStore = this.store.namespace(id) const room = new ChatRoom(roomStore, this.swarm, { name, info }) this.rooms[id] = room this._watchMessages(id) await room.ready() await room.addRoomInfo() await this.base.append( ChatDispatch.encode('@pear-chat-multi-rooms/add-room', { id, name: room.name, invite: room.invite, info: room.info }) ) } async joinRoom (invite) { const id = Math.random().toString(16).slice(2) const roomStore = this.store.namespace(id) const room = new ChatRoom(roomStore, this.swarm, { invite }) this.rooms[id] = room room.on('update', async () => { const remoteRoom = await room.getRoomInfo() if (remoteRoom && remoteRoom.name !== room.name) { room.name = remoteRoom.name room.info = remoteRoom.info await this.base.append( ChatDispatch.encode('@pear-chat-multi-rooms/add-room', { id, name: room.name, invite, info: room.info }) ) } }) this._watchMessages(id) await room.ready() } ``` The key change: each room is an independent Autobase in its own namespace, so the account never co-mingles room data. The account base only stores room metadata (id, name, invite, info), encrypted by the account's own Autobase. See [Storage and distribution](/explanation/storage-and-distribution) for the underlying mental model. Extend the schema [#extend-the-schema] This step touches all three builders in `schema.js`, so it is easiest to follow the full [`pear-chat-multi-rooms` `schema.js`](https://github.com/holepunchto/pear-docs/blob/preview/examples/how-to/connect-to-peers/multi-rooms/schema.js). The changes are: 1. **Rename the namespace** from `pear-chat` to `pear-chat-multi-rooms` in all three `.namespace(...)` calls (schema, db, dispatch). The type references below (`@pear-chat-multi-rooms/...`) resolve against this name—if you leave the namespace as `pear-chat`, `node schema.js` throws `TypeError: Cannot read properties of undefined (reading 'frameable')` because the referenced type does not exist. 2. **Register** a `room` schema (`{ id, name, invite, info }`), plus a `rooms` HyperDB collection and an `add-room` HyperDispatch entry. The roomId that the renderer uses to address a specific room is carried on the plain-JSON messages exchanged over the worker pipe, so the schema only needs to persist the room metadata. The new `room` registration looks like this (L26–L34): ```js file=/examples/how-to/connect-to-peers/multi-rooms/schema.js#L26-L34 title="schema.js" lineNumbers {26-34} skip="example-import" schema.register({ name: 'room', fields: [ { name: 'id', type: 'string', required: true }, { name: 'name', type: 'string', required: true }, { name: 'invite', type: 'string', required: true }, { name: 'info', type: 'json' } ] }) ``` Regenerate `spec/` from a **clean** directory: ```bash skip="desktop-gui" rm -rf spec && npm run build:db ``` Delete `spec/` before regenerating. The schema generators (`hyperschema`, `hyperdispatch`, `hyperdb`) **merge** into the existing manifests rather than overwriting them, so regenerating on top of the old `pear-chat` spec leaves stale registrations next to the new `pear-chat-multi-rooms` ones—the generated `spec/` then carries duplicate definitions. Update the renderer [#update-the-renderer] In [`renderer/index.html`](https://github.com/holepunchto/pear-docs/blob/preview/examples/how-to/connect-to-peers/multi-rooms/renderer/index.html) and [`renderer/app.js`](https://github.com/holepunchto/pear-docs/blob/preview/examples/how-to/connect-to-peers/multi-rooms/renderer/app.js), split the layout into a left rail (the room list) and a right pane (the active room). The renderer: 1. sends `{ type: 'add-room', name }` and `{ type: 'join-room', invite }` over the worker pipe, 2. renders the left rail from the `{ type: 'rooms', rooms }` events the worker pushes, 3. the existing message send becomes `{ type: 'add-message', text, roomId }`, and 4. incoming `{ type: 'messages', roomId, messages }` events are filed under their `roomId`, so the renderer just shows the selected room's messages. Run it [#run-it] ```bash skip="desktop-gui" npm run build npm start -- --storage /tmp/multi-user1 --name user1 ``` Type a room name and click **Create**. Select the room in the left rail, then click **Copy invite** in the room header to copy the **room invite** (not the `Account Invite` line in the terminal—that pairs a whole account, not a single room). In a second terminal: ```bash skip="desktop-gui" npm start -- --storage /tmp/multi-user2 --name user2 --invite ``` Or start without `--invite` and paste the room invite into the join field in the UI. user2's app shows the joined room in its left rail. Both peers can now create or join additional rooms—each one is an independent [Autobase](/reference/building-blocks/autobase) with its own pairing flow. Where to go next [#where-to-go-next] * [Add blind peering to a chat app](/how-to/blind-peering/add-blind-peering-to-a-chat-app)—keep individual rooms reachable when their writers are offline. * [Work with many Hypercores using Corestore](/how-to/store-and-replicate/work-with-many-hypercores-using-corestore)—the pattern that makes hosting many rooms in one app cheap. * [Storage and distribution](/explanation/storage-and-distribution)—why each room can be its own [Autobase](/reference/building-blocks/autobase) without paying per-room infrastructure costs. # Connect to peers import { Cards, Card } from 'fumadocs-ui/components/card' Recipes for finding other peers and opening direct, end-to-end-encrypted connections. # Add Keet identity to a chat app import { Steps, Step } from 'fumadocs-ui/components/steps' This guide shows you how to **add a Keet-style portable identity** to [`pear-chat`](https://github.com/holepunchto/pear-docs/tree/preview/examples/getting-started/pear-chat) so a user's identity survives across devices and reinstalls. The reference implementation is [`pear-chat-identity`](https://github.com/holepunchto/pear-docs/tree/preview/examples/how-to/manage-identity/keet-identity). {/* Shared snippet (content/_snippets/) included via Fumadocs by the chat-app delta how-tos. Reminds readers these guides teach portable Pear-end (worker) logic, not desktop-specific code. See: https://www.fumadocs.dev/docs/markdown#include */} **This guide is about the Pear-end, not the shell.** The code below lives in the Bare [worker](/explanation/workers)—the peer-to-peer logic, not the user interface. Because the Pear-end never imports DOM APIs and never assumes a UI framework, the same worker is portable across **desktop (Electron)**, **mobile (React Native via Bare iOS / Bare Android)**, and **terminal**. The example apps ship an Electron shell, but only the UI half changes per platform—the logic here stays the same. See [Runtime and languages](/explanation/runtime-and-languages) for the cross-platform model and current support. This is a **delta-only** how-to. The shared scaffold is explained in the [Reshape into a production app](/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app) tutorial—read it first. Before you begin [#before-you-begin] * A working clone of `pear-chat` (or your own app built from the getting-started path). * Comfort with [Corestore](/reference/helpers/corestore) and the [Autobase](/reference/building-blocks/autobase)-backed room model. What changes [#what-changes] | Layer | Change | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | Dependencies | Add [`keet-identity-key`](https://www.npmjs.com/package/keet-identity-key) and [`hypercore-crypto`](https://www.npmjs.com/package/hypercore-crypto). | | Worker | Generate or load a mnemonic, derive a Keet identity key inside `WorkerTask`, and use it to sign every message. | | Schema | Extend the `message` struct so each message carries a `proof` field that ties it to a stable identity. | The Electron shell, worker transport (plain JSON over a `FramedStream`), and vanilla renderer stay as in the getting-started chat app. Steps [#steps] Add the dependencies [#add-the-dependencies] ```bash skip="desktop-gui" npm install keet-identity-key hypercore-crypto ``` Persist a mnemonic [#persist-a-mnemonic] In `workers/index.js`, resolve the mnemonic before constructing `WorkerTask`. A `--mnemonic` flag wins if supplied (L27); otherwise read `identity-mnemonic.txt` from the app storage directory (L28, L30), and if that file does not exist yet, generate a fresh 24-word phrase with [`keet-identity-key`](https://www.npmjs.com/package/keet-identity-key) (L33). Persist it back (L35) so every subsequent start loads the same identity. Treat the mnemonic file as **sensitive**—do not check it into version control, and back it up the way you would a wallet seed. ```js file=/examples/how-to/manage-identity/keet-identity/workers/index.js#L27-L35 title="workers/index.js" lineNumbers {27,28,30,33,35} skip="example-import" let mnemonic = cmd.flags.mnemonic const mnemonicPath = path.join(appStorage, 'identity-mnemonic.txt') if (!mnemonic) { mnemonic = await fs.promises.readFile(mnemonicPath, 'utf-8').catch((err) => { if (err.code !== 'ENOENT') throw err }) mnemonic = mnemonic || Identity.generateMnemonic() } await fs.promises.writeFile(mnemonicPath, mnemonic) ``` Derive an identity and attest a device inside WorkerTask [#derive-an-identity-and-attest-a-device-inside-workertask] Pass the resolved mnemonic into `WorkerTask` as a constructor argument (`new WorkerTask(pipe, storage, mnemonic, cmd.flags)`). In `workers/worker-task.js`, the constructor stores the mnemonic (L16) and builds a `ChatRoomIdentity` room (L24). In `_open`, load the identity from that mnemonic and bootstrap a per-device key pair the identity attests (L34–L36). Each appended message is signed with the device key via `Identity.attestData` (L49–L50), so the worker stamps every line with a verifiable proof—and `_messages` verifies each proof with `Identity.verify` (L68–L71) before forwarding messages to the renderer. The [`pear-chat-identity`](https://github.com/holepunchto/pear-docs/tree/preview/examples/how-to/manage-identity/keet-identity) example keeps the original storage-namespaced [Autobase](/reference/building-blocks/autobase) (via `ChatRoomIdentity`) but extends the message schema to carry the proof: ```js file=/examples/how-to/manage-identity/keet-identity/workers/worker-task.js title="workers/worker-task.js" lineNumbers {16,24,34-36,49-50,68-71} skip="example-import" const Corestore = require('corestore') const debounce = require('debounceify') const crypto = require('hypercore-crypto') const Hyperswarm = require('hyperswarm') const Identity = require('keet-identity-key') const ReadyResource = require('ready-resource') const ChatRoomIdentity = require('./chat-room-identity') class WorkerTask extends ReadyResource { constructor (pipe, storage, mnemonic, opts = {}) { super() this.pipe = pipe this.storage = storage this.mnemonic = mnemonic this.invite = opts.invite this.name = opts.name || `User ${Date.now()}` this.store = new Corestore(storage) this.swarm = new Hyperswarm() this.swarm.on('connection', (conn) => this.store.replicate(conn)) this.room = new ChatRoomIdentity(this.store, this.swarm, this.invite) this.debounceMessages = debounce(() => this._messages()) this.room.on('update', () => this.debounceMessages()) this.identity = null this.deviceKeyPair = null this.deviceProof = null } async _open () { this.identity = await Identity.from({ mnemonic: this.mnemonic }) this.deviceKeyPair = crypto.keyPair() this.deviceProof = await this.identity.bootstrap(this.deviceKeyPair.publicKey) await this.store.ready() await this.room.ready() this.pipe.on('data', async (data) => { let message try { message = JSON.parse(data) } catch { return } if (message.type === 'add-message') { const proof = Identity.attestData(Buffer.from(message.text), this.deviceKeyPair, this.deviceProof) await this.room.addMessage(message.text, proof, { name: this.name, at: Date.now() }) } }) await this.debounceMessages() this.pipe.write(JSON.stringify({ type: 'invite', invite: await this.room.getInvite() })) } async _close () { await this.room.close() await this.swarm.destroy() await this.store.close() } async _messages () { const messages = await this.room.getMessages() messages.sort((a, b) => a.info.at - b.info.at) for (const msg of messages) { const res = Identity.verify(msg.proof, Buffer.from(msg.text), { expectedIdentity: this.identity.identityPublicKey }) msg.info.verified = !!res } this.pipe.write(JSON.stringify({ type: 'messages', messages })) } } module.exports = WorkerTask ``` Add the identity-aware room [#add-the-identity-aware-room] `worker-task.js` now imports `ChatRoomIdentity` instead of the tutorial's `ChatRoom`. Create `workers/chat-room-identity.js` as a copy of the tutorial's [`chat-room.js`](https://github.com/holepunchto/pear-docs/blob/preview/examples/getting-started/pear-chat/workers/chat-room.js) with three changes: 1. Rename the class to `ChatRoomIdentity` (L11), 2. Rename its `@pear-chat/*` HyperDB/HyperDispatch collections to `@pear-chat-identity/*` (L111, L114, L117), and 3. Widen `addMessage` to take and persist a `proof` alongside each message (L149–L153). Without this file the worker fails to boot with `MODULE_NOT_FOUND: ./chat-room-identity`: ```js file=/examples/how-to/manage-identity/keet-identity/workers/chat-room-identity.js title="workers/chat-room-identity.js" lineNumbers {11,111,114,117,149-153} skip="example-import" const Autobase = require('autobase') const b4a = require('b4a') const BlindPairing = require('blind-pairing') const HyperDB = require('hyperdb') const ReadyResource = require('ready-resource') const z32 = require('z32') const ChatDispatch = require('../spec/dispatch') const ChatDb = require('../spec/db') class ChatRoomIdentity extends ReadyResource { constructor (store, swarm, invite) { super() this.store = store this.swarm = swarm this.invite = invite this.pairing = new BlindPairing(swarm) /** @type {{ add: function(string, function(any, { view: HyperDB, base: Autobase })) }} */ this.router = new ChatDispatch.Router() this._setupRouter() this.localBase = Autobase.getLocalCore(this.store) this.base = null this.pairMember = null } async _open () { await this.localBase.ready() const localKey = this.localBase.key const isEmpty = this.localBase.length === 0 let key let encryptionKey if (isEmpty && this.invite) { const res = await new Promise((resolve) => { this.pairing.addCandidate({ invite: z32.decode(this.invite), userData: localKey, onadd: resolve }) }) key = res.key encryptionKey = res.encryptionKey } // if base is not initialized, key and encryptionKey must be provided // if base is already initialized in this store namespace, key and encryptionKey can be omitted await this.localBase.close() this.base = new Autobase(this.store, key, { encrypt: true, encryptionKey, open: this._openBase.bind(this), close: this._closeBase.bind(this), apply: this._applyBase.bind(this) }) const writablePromise = new Promise((resolve) => { this.base.on('update', () => { if (this.base.writable) resolve() if (!this.base._interrupting) this.emit('update') }) }) await this.base.ready() this.swarm.join(this.base.discoveryKey) if (!this.base.writable) await writablePromise this.view.core.download({ start: 0, end: -1 }) this.pairMember = this.pairing.addMember({ discoveryKey: this.base.discoveryKey, /** @type {function(import('blind-pairing-core').MemberRequest)} */ onadd: async (request) => { const inv = await this.view.findOne('@pear-chat-identity/invites', { id: request.inviteId }) if (!inv) return request.open(inv.publicKey) await this.addWriter(request.userData) request.confirm({ key: this.base.key, encryptionKey: this.base.encryptionKey }) } }) } async _close () { await this.pairMember?.close() await this.base?.close() await this.localBase.close() await this.pairing.close() } _openBase (store) { return HyperDB.bee(store.get('view'), ChatDb, { extension: false, autoUpdate: true }) } async _closeBase (view) { await view.close() } async _applyBase (nodes, view, base) { for (const node of nodes) { await this.router.dispatch(node.value, { view, base }) } await view.flush() } _setupRouter () { this.router.add('@pear-chat-identity/add-writer', async (data, context) => { await context.base.addWriter(data.key) }) this.router.add('@pear-chat-identity/add-invite', async (data, context) => { await context.view.insert('@pear-chat-identity/invites', data) }) this.router.add('@pear-chat-identity/add-message', async (data, context) => { await context.view.insert('@pear-chat-identity/messages', data) }) } /** @type {HyperDB} */ get view () { return this.base.view } async getInvite () { const existing = await this.view.findOne('@pear-chat-identity/invites', {}) if (existing) { return z32.encode(existing.invite) } const { id, invite, publicKey, expires } = BlindPairing.createInvite(this.base.key) await this.base.append( ChatDispatch.encode('@pear-chat-identity/add-invite', { id, invite, publicKey, expires }) ) return z32.encode(invite) } async addWriter (key) { await this.base.append( ChatDispatch.encode('@pear-chat-identity/add-writer', { key: b4a.isBuffer(key) ? key : b4a.from(key) }) ) } async getMessages ({ reverse = true, limit = 100 } = {}) { return await this.view.find('@pear-chat-identity/messages', { reverse, limit }).toArray() } async addMessage (text, proof, info) { const id = Math.random().toString(16).slice(2) await this.base.append( ChatDispatch.encode('@pear-chat-identity/add-message', { id, text, proof, info }) ) } } module.exports = ChatRoomIdentity ``` Extend the schema [#extend-the-schema] `chat-room-identity.js` references a `pear-chat-identity` namespace and a new `proof` field, so update `schema.js` to match: 1. Rename the namespace from `pear-chat` to `pear-chat-identity`, 2. Add a `proof` field (`type: 'buffer'`) to the `message` struct that feeds the `messages` collection, and 3. Then regenerate `spec/` from a **clean** directory: ```bash skip="desktop-gui" rm -rf spec && npm run build:db ``` Delete `spec/` before regenerating. The schema generators (`hyperschema`, `hyperdispatch`, `hyperdb`) **merge** into the existing manifests rather than overwriting them, so if you regenerate on top of the old `pear-chat` spec the stale registrations linger alongside the new `pear-chat-identity` ones. Starting from a clean `spec/` keeps only the `pear-chat-identity` namespace. `ChatRoomIdentity.addMessage` then stores the `proof` alongside each message. `_messages` verifies each one with `Identity.verify(msg.proof, Buffer.from(msg.text), { expectedIdentity: this.identity.identityPublicKey })`, so anyone replicating the room can confirm which identity authored each line—across reinstalls and across machines, since the same mnemonic always yields the same identity. Surface the identity in the UI [#surface-the-identity-in-the-ui] The worker already verifies every message in `_messages` and stamps `msg.info.verified` (step 3), and that boolean rides along with each message in the same `{ type: 'messages', messages }` JSON payload the renderer already receives. So the renderer needs **no new message type**—it just reads the extra field. In `renderer/app.js`, each message row reads `message.info?.verified` (L43) and renders a verified/unverified badge next to the sender's name (L44–L47), then appends it to the row's metadata line (L53): ```js file=/examples/how-to/manage-identity/keet-identity/renderer/app.js#L43-L54 title="renderer/app.js" lineNumbers {43-47,53} skip="example-import" const verified = message.info?.verified const badge = document.createElement('span') badge.className = `text-[10px] ${verified ? 'text-emerald-400' : 'text-rose-400'}` badge.title = verified ? 'Signature verified' : 'Signature invalid' badge.textContent = verified ? '● verified' : '○ unverified' const time = document.createElement('span') time.className = 'ml-auto text-xs text-neutral-500' time.textContent = new Date(message.info?.at).toLocaleTimeString() meta.append(name, badge, time) ``` The badge turns green only when the proof on that message validates against the identity that authored it—so a peer replicating the room can see, per line, which messages are provably from a given identity across reinstalls and devices. Run it [#run-it] ```bash skip="desktop-gui" npm run build npm start -- --storage /tmp/identity-user1 --name user1 ``` In development, `electron/main.js` namespaces `--storage ` by your app's `productName` (from `package.json`—it's `PearChat` if you're building on top of the getting-started app) and the worker writes into an `app-storage` subdirectory. So with `--storage /tmp/identity-user1` the files land at `/tmp/identity-user1//app-storage/`: * `…/app-storage/corestore/`—the [Hypercore](/reference/building-blocks/hypercore) data * `…/app-storage/identity-mnemonic.txt`—the mnemonic, a sibling of `corestore/` The `app-storage` folder only appears once the app worker has run (the [`pear-runtime`](/reference/pear/runtime) folder next to it is the separate updater store). The exact path is easiest to copy from the terminal: the worker logs a `Storage: …/app-storage/corestore` line on startup, and the mnemonic sits next to that `corestore/` directory. Quit the app, blow away the [Corestore](/reference/helpers/corestore) but **keep `identity-mnemonic.txt`** (substitute your own `productName` for ``): ```bash skip="desktop-gui" rm -rf /tmp/identity-user1//app-storage/corestore npm start -- --storage /tmp/identity-user1 --name user1 ``` user1's identity key is unchanged. Copy `identity-mnemonic.txt` into another machine's matching `app-storage` directory and the same identity follows there. Where to go next [#where-to-go-next] * [Create a portable identity with Keet identity keys](/how-to/manage-identity/create-a-portable-identity-with-keet-identity-key)—the Pear/Bare identity primitive behind this app, with no UI. * [Connect two peers by key with HyperDHT](/how-to/connect-to-peers/connect-two-peers-by-key-with-hyperdht)—once you have an identity key, you can dial it directly. * [Add blind peering to a chat app](/how-to/blind-peering/add-blind-peering-to-a-chat-app)—keep the room reachable while the identity-holding device is offline. * [Workers](/explanation/workers)—why the identity lives in the worker, not the renderer. # Create a portable identity with Keet identity keys **This guide focuses on the Pear/Bare logic.** It shows [`keet-identity-key`](https://www.npmjs.com/package/keet-identity-key) on its own—no Electron, no UI. For the same identity wired into a full desktop chat app, see the worked example [`pear-chat-identity`](https://github.com/holepunchto/pear-docs/tree/preview/examples/how-to/manage-identity/keet-identity) and its walkthrough, [Add Keet identity to a chat app](/how-to/manage-identity/add-keet-identity-to-a-chat-app). A [Hypercore](/reference/building-blocks/hypercore) key identifies a *log*, not a *person*. [`keet-identity-key`](https://www.npmjs.com/package/keet-identity-key) gives you a **portable identity**: a key derived from a 24-word mnemonic that stays the same across devices and reinstalls. Each device generates its own throwaway key pair, which the identity **attests**—so you can sign data on any device and anyone can verify it was authored by the same person, without ever copying the identity's secret onto that device. Without the identity, you would need to share a secret between devices to sign data—and that secret would need to be copied onto each device. With the identity, you can sign data on any device and anyone can verify it was authored by the same person, without ever copying the identity's secret onto that device. This is purely Pear-end logic: it runs in a [Bare](/reference/modules/bare-modules) worker (or any Bare/Node process) and never touches a UI. {/* Shared snippet (content/_snippets/) included via Fumadocs by how-tos whose Pear-end (worker) logic works with EITHER boilerplate (desktop or terminal). Carries the cross-platform portability point too, so it REPLACES _pear-end-portability-callout on these guides—one orientation callout, not two. For frontend-specific how-tos, use _frontend-template-callout.mdx instead. See: https://www.fumadocs.dev/docs/markdown#include */} **Pear-end logic—start from a boilerplate.** The code in this guide lives in the Bare [worker](/explanation/workers): peer-to-peer logic, no UI. The same worker runs unchanged on desktop, terminal, and mobile—only the shell differs (see [Runtime and languages](/explanation/runtime-and-languages)). Start from a boilerplate in [Start from a template](/getting-started/from-a-template)—desktop ([`hello-pear-electron`](/getting-started/from-a-template/start-from-hello-pear-electron)) or terminal ([`hello-pear-bare`](/getting-started/from-a-template/start-from-hello-pear-bare))—and add this capability on top. The model [#the-model] | Concept | What it is | | ------------------- | ------------------------------------------------------------------------------- | | **Mnemonic** | 24 words. The root secret—back it up like a wallet seed. | | **Identity** | Derived from the mnemonic. Its `identityPublicKey` is stable everywhere. | | **Device key pair** | A fresh, per-device key. Never leaves the device. | | **Device proof** | The identity's signature attesting that the device key speaks for it. | | **Data proof** | A signature over a payload, made with the device key, anchored to the identity. | Add the dependencies [#add-the-dependencies] ```sh skip="install-instruction" npm install keet-identity-key hypercore-crypto ``` Derive the identity and attest a device [#derive-the-identity-and-attest-a-device] Generate (or load) a mnemonic (L5), derive the identity from it (L7), then mint a fresh per-device key pair (L11) and bootstrap it—the identity attests the device key and returns a `deviceProof` (L12): ```js file=/examples/how-to/manage-identity/create-a-portable-identity-with-keet-identity-key/index.js#L1-L12 title="index.js" lineNumbers {5,7,11-12} skip="example-import" import Identity from 'keet-identity-key' import crypto from 'hypercore-crypto' // Generate once and persist it somewhere safe (treat it like a wallet seed). const mnemonic = Identity.generateMnemonic() const identity = await Identity.from({ mnemonic }) // identity.identityPublicKey is the same on every device that loads this mnemonic. // Each device gets its own ephemeral key pair, attested by the identity. const deviceKeyPair = crypto.keyPair() const deviceProof = await identity.bootstrap(deviceKeyPair.publicKey) ``` Persist the mnemonic, not the device key—a new device re-derives the identity from the same mnemonic and bootstraps a fresh device key of its own. Sign data [#sign-data] Take a payload (L15) and attest it with the device key. The resulting proof (L16) carries the chain back to the identity: ```js file=/examples/how-to/manage-identity/create-a-portable-identity-with-keet-identity-key/index.js#L15-L16 title="index.js" lineNumbers {15-16} skip="example-import" const payload = Buffer.from('hello from this device') const proof = Identity.attestData(payload, deviceKeyPair, deviceProof) ``` Attach `proof` to whatever you append to your [Hypercore](/reference/building-blocks/hypercore)/Autobase alongside the payload. Verify data [#verify-data] Any peer replicating the data can verify it was authored by the expected identity—pass the proof, the payload, and the expected `identityPublicKey`, and `verify` returns truthy only for a valid proof (L19–L21). No shared secret needed: ```js file=/examples/how-to/manage-identity/create-a-portable-identity-with-keet-identity-key/index.js#L19-L22 title="index.js" lineNumbers {19-21} skip="example-import" const ok = Identity.verify(proof, payload, { expectedIdentity: identity.identityPublicKey }) // ok is truthy when the proof is valid for that identity. ``` Because verification only needs the public `identityPublicKey`, you can stamp every message with a proof and let every reader confirm authorship across reinstalls and across machines. See also [#see-also] * [Add Keet identity to a chat app](/how-to/manage-identity/add-keet-identity-to-a-chat-app)—this primitive wired into a full desktop chat, stamping every message with a verifiable identity. * [Connect two peers by key with HyperDHT](/how-to/connect-to-peers/connect-two-peers-by-key-with-hyperdht)—once you have a stable identity key, you can dial it directly. * [Secretstream](/reference/helpers/secretstream)—the noise-based encrypted transport that authenticates connections by public key. * [Workers](/explanation/workers)—why identity belongs in the worker, not the renderer. # Manage identity import { Cards, Card } from 'fumadocs-ui/components/card' Recipes for giving users a portable cryptographic identity that outlives any single device. # Release & distribute your app import { Cards, Card } from 'fumadocs-ui/components/card' Everything for shipping a Pear app to users and keeping it updated over the air. For the conceptual picture behind the flow, see [Release pipeline](/explanation/deployment-releasing-apps-p2p). # Migrate from pear run to Pear OTA import { Steps, Step } from 'fumadocs-ui/components/steps' **`pear run` was removed in Pear v3**, along with the ambient global `Pear` API. An unmigrated app is at its end of life—running `pear run` now exits with an error. Migrate onto **[Pear OTA](/reference/pear/runtime)** (the `pear-runtime` library) to restore updates. Pear has evolved from a runtime baked into the [Pear CLI](/reference/pear/cli) (`pear run`) into an embeddable runtime library, **[Pear OTA](/reference/pear/runtime)** (the `pear-runtime` module), that integrates into any JavaScript environment. The CLI keeps the deployment commands (`pear stage`, `pear build`, `pear provision`, `pear multisig`); the runtime moves into your own process. This exposes peer-to-peer over-the-air updates and Bare workers to any JS project instead of locking them behind the CLI runtime. This page is the operator how-to for moving an existing v1 app across. To upgrade the CLI itself from v2, run `npx pear`—see [Install & upgrade](/reference/pear/cli#install) in the CLI reference. For the background on why this changed, see [Runtime and languages](/explanation/runtime-and-languages). Why this changed [#why-this-changed] Running every app on a single vendor-signed Electron build made shipping trivial—no OS signing keys, no native compiling—but it turned out to be fragile. The same build served many apps, so it could not be tested against each one before an update; OS updates, virus scanners, and process managers did not expect the non-standard runtime; and Dock/Tray branding was glitchy (a restarted app could show the Pear icon). Splitting the runtime into a library lets the CLI focus on deployment and productionization while your app owns a standard, testable runtime. What changes [#what-changes] | `pear run` (removed) | Pear OTA — `pear-runtime` | | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | The CLI runtime launches the app and provides the ambient `Pear` global | You import [`pear-runtime`](/reference/pear/runtime) in your own JS entrypoint | | `pear stage` deploys the app contents | [`pear build`](https://github.com/holepunchto/hello-pear-electron#build-deploy-directory) produces a build folder that *is* the deployment folder | | `global.Pear.worker.run()` spawns workers | [`pear-runtime`](/reference/pear/runtime)'s `run()` method (backed by `bare-sidecar`, which bundles the native Bare runtime) | Steps [#steps] Start from the boilerplate [#start-from-the-boilerplate] The fastest migration is to scaffold a fresh Electron app that already has `pear-runtime` integrated, then move your code into it. Use the [`hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron) boilerplate: ```bash skip="manual-walkthrough" git clone https://github.com/holepunchto/hello-pear-electron cd hello-pear-electron npm install npm start ``` Move your HTML, JS, CSS, assets, and dependencies into the template's structure. For a guided tour of where each piece goes, see [Start from the hello-pear-electron template](/getting-started/from-a-template/start-from-hello-pear-electron). The build directory layout `pear-runtime` expects is produced by the [`pear build`](https://github.com/holepunchto/hello-pear-electron#build-deploy-directory) command (Pear v2.5.0+). Your `package.json` needs an `upgrade` field set to the application's `pear://` link—see [Configuration](/reference/pear/configuration#packagejson). Remove the global Pear API [#remove-the-global-pear-api] Any code using the `Pear` global must be removed and replaced. For Electron: | v1 global `Pear` API | v2 replacement | | -------------------------- | -------------------------------------------------------------------------------- | | `global.Pear.worker.run()` | [`pear-runtime`](/reference/pear/runtime) `run()` | | `global.Pear.updates()` | `pear.updater` `updating`/`updated` events | | `global.Pear.exit()` | `process.exit()` | | `global.Pear.exitCode` | `process.exitCode` | | `global.Pear.restart()` | `app.relaunch()` (Electron) | | `global.Pear.teardown()` | [`graceful-goodbye`](https://github.com/holepunchto/graceful-goodbye) or similar | | `global.Pear.app.args` | `process.argv.slice(2)` | | `global.Pear.app.applink` | `require('./package.json').upgrade` | The remaining state on `global.Pear.app` (which supersedes the deprecated `global.Pear.config`) is either self-referential (no longer relevant) or link parsing—replace link parsing with direct deep-link protocol handling. Deploy with the production flow [#deploy-with-the-production-flow] `pear-runtime` pairs with the CLI's deployment commands rather than `pear run`. Use `pear stage` for internal previews, staging, and ephemeral variations—**not** for production. For production rigour, run `pear stage`, [`pear provision`](/reference/pear/cli#pear-provision), and `pear multisig` in sequence: * [`pear provision`](/reference/pear/cli#pear-provision) (Pear v2.6.0+) strips interim operations by block-syncing from a source drive, reducing the disk space an app needs. Use it to create a prerelease. * [`pear multisig`](/reference/pear/cli#pear-multisig) manages cryptographic cosigning: define the signers, define the quorum, then sign against the prerelease. Until you migrate to this deployment flow, you risk losing write access if something happens to the machine holding the key—with no way to recover, because there is no way to push an update that switches to a new application link. Unlike all other Pear application drives, a `pear multisig`'d drive is **not** machine-bound, which removes that single point of failure. Use `pear multisig` for any serious production sign-off. See [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment) for the full step-by-step release flow. See also [#see-also] * [`pear-runtime` reference](/reference/pear/runtime)—the module that replaces the global `pear run` API. * [Start from a template](/getting-started/from-a-template)—the desktop and terminal boilerplates to migrate into. * [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment)—the stage → provision → multisig production flow. * [Runtime and languages](/explanation/runtime-and-languages)—why the runtime moved out of the CLI and into a library. * [Configuration](/reference/pear/configuration#packagejson)—where the `upgrade` link and entrypoint are declared. # Publish a changelog for your app import { Steps, Step } from 'fumadocs-ui/components/steps' Any Pear application can ship a `CHANGELOG.md`. Stage it with the rest of your project and your users—or any third party—read your release notes straight from the app drive with [`pear changelog pear://`](/reference/pear/cli#pear-changelog), semver-filtered and pretty-printed. It is the same command that prints the Pear platform's own changelog when you run [`pear changelog`](/reference/pear/cli#pear-changelog) with no link (a known regression makes the no-link form error on some 3.0.x builds—pass an explicit `pear://`, as this guide does throughout). The changelog lives in the app's [Hyperdrive](/reference/building-blocks/hyperdrive), so it is versioned, replicated, and readable peer-to-peer with no server and no separate publishing step—it travels with every stage and provision. This is your **application's own** changelog. It is distinct from the docs-team [Release Overview](/release-overview), which is a curated view of changes across Pear and its modules. To publish release notes for *your* app, follow this guide. {/* Shared snippet (content/_snippets/) included via Fumadocs by guides that run `pear` CLI commands, so readers can install the CLI before following the steps. See: https://www.fumadocs.dev/docs/markdown#include */} **Need the `pear` CLI?** Install it from **[install.pears.com](https://install.pears.com)**, or prefix any command below with `npx`. See [Install & upgrade](/reference/pear/cli#install) for details. How it works [#how-it-works] `pear changelog` reads a single file at the **root of your app drive**: `/CHANGELOG.md`. Under the hood the [`pear-changelog`](https://github.com/holepunchto/pear-changelog) module parses it into one entry per release, then the command filters by version and prints each entry's notes. The filename and drive-root location are the entire contract—there is no manifest field to set and nothing to register. Write CHANGELOG.md [#write-changelogmd] Create a `CHANGELOG.md` at your **project root** (next to `package.json`). The format is lightweight: * A header block at the top (typically a `# Title`)—everything before the first `##` heading is ignored by the parser. Don't start the file with a `##` heading: the parser only recognizes headings that follow a line, so the first release would be dropped. * One release per level-2 heading (`## `), **newest first**. * The **first space-separated token of each heading must be a [SemVer](https://semver.org/) version** (a leading `v` is allowed and stripped). Everything under a heading until the next `##` is free-form Markdown. ```md file=/examples/how-to/operate-an-app/publish-a-changelog/CHANGELOG.md title="CHANGELOG.md" skip="example-import" # Acme Chat Changelog ## v1.2.0 ### Features - Group threads with @mentions. ### Fixes - Reconnect automatically after the sidecar restarts. ## v1.1.0 ### Features - Full-text message search. ## v1.0.0 Initial release. ``` Keep the version as the **first word** of the heading, followed by a space. Bracketed [Keep a Changelog](https://keepachangelog.com/) headings such as `## [1.2.0] - 2026-01-01` aren't valid versions—those releases drop out of the default view and of any `--of` range (only `--full` still shows them). Use `## v1.2.0` or `## 1.2.0` instead. You can still add a date after the version, for example, `## v1.2.0 — 2026-01-01`. Check the format locally [#check-the-format-locally] Before staging, confirm each release parses—with the same [`pear-changelog`](https://github.com/holepunchto/pear-changelog) module the CLI uses. This checks that headings are *recognized*; it does not apply the SemVer filter, so keep the version-format rules above in mind too. Install it as a dev dependency: {/* @harness example=publish-changelog step=copy from=examples/how-to/operate-an-app/publish-a-changelog/package.json to=package.json */} {/* @harness example=publish-changelog step=copy from=examples/how-to/operate-an-app/publish-a-changelog/CHANGELOG.md to=CHANGELOG.md */} ```sh example=publish-changelog step=setup npm install --save-dev pear-changelog ``` Create `check-changelog.js` next to the changelog: ```js file=/examples/how-to/operate-an-app/publish-a-changelog/check-changelog.js title="check-changelog.js" skip="example-import" const { readFileSync } = require('node:fs') const { parse } = require('pear-changelog') for (const [version] of parse(readFileSync('CHANGELOG.md'))) { console.log('release:', version) } ``` {/* @harness example=publish-changelog step=copy from=examples/how-to/operate-an-app/publish-a-changelog/check-changelog.js to=check-changelog.js */} Run it with Node: ```sh example=publish-changelog step=run process=check expect="release: v1.0.0" node check-changelog.js ``` {/* @harness example=publish-changelog step=expect process=check contains="release: v1.2.0" */} {/* @harness example=publish-changelog step=expect process=check contains="release: v1.1.0" */} It prints one line per release the parser found, newest first: ```text skip="sample-output" release: v1.2.0 release: v1.1.0 release: v1.0.0 ``` If a release is missing from the output, its heading didn't parse—`pear changelog` won't show it either. The reverse isn't guaranteed: a release can parse here yet still be filtered out by `pear changelog` when its version isn't valid SemVer (see the format warning above). Stage and seed it with your app [#stage-and-seed-it-with-your-app] A root `CHANGELOG.md` is staged like any other project file—no extra flags. Just make sure it is not excluded by a [`pear.stage.ignore`](/reference/pear/configuration#pear-stage-ignore) entry or dropped by a [`pear.stage.only`](/reference/pear/configuration#pear-stage-only) filter that omits it. Deploying a desktop app? The deployment directory that [`pear build`](/reference/pear/cli#pear-build) assembles contains only `package.json` and `by-arch/`—your project's `CHANGELOG.md` is not copied in. Add it before staging: `cp CHANGELOG.md ../-/`. ```bash skip="release-flow" pear stage --dry-run pear:// # confirm CHANGELOG.md appears in the diff pear stage pear:// ``` Then [`pear seed`](/reference/pear/cli#pear-seed) the link so peers can fetch it. Once staged, the file is retrievable at `pear:///CHANGELOG.md`. Bump `CHANGELOG.md` in the same commit as your version bump so each release carries its own notes—see [Ship your app](/getting-started/build-a-peer-to-peer-chat/ship) for the full stage → provision flow. Read it back [#read-it-back] Anyone with the link reads your changelog with: ```bash skip="live-swarm" pear changelog pear:// ``` By default this prints up to the **10 newest releases within the current major version**, each with its full notes, separated by a divider. Tune it with the flags: | Flag | Effect | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `--of ` | Filter to a version range, for example, `--of 1.x.x` or `--of '>=1.1.0 <2.0.0'`. Defaults to the latest major. | | `--max`, `-m ` | Cap the number of entries shown (default `10`). | | `--full` | Show every release (overrides `--max`); without `--of`, it also widens the range to all majors. | | `--json` | Emit newline-delimited JSON—one tagged object per release plus a final status object. Filter on `"tag": "changelog"` when scripting. | ```bash skip="live-swarm" pear changelog pear:// --of 1.x.x # only the 1.x releases pear changelog pear:// --full # the entire history pear changelog pear:// --json # machine-readable ``` Use it for release announcements [#use-it-for-release-announcements] Because the changelog is queryable by version range, it doubles as your source of truth for release notes. When you cut a release, pull the exact entries for the range you shipped and paste them into your announcement: ```bash skip="live-swarm" pear changelog pear:// --of '>=1.1.0' # everything since 1.1.0 ``` Relationship to pear dump [#relationship-to-pear-dump] `pear changelog` is the structured view of the same file that [`pear dump`](/reference/pear/cli#pear-dump) returns raw: ```bash skip="live-swarm" pear dump pear:///CHANGELOG.md - # raw Markdown, unparsed, to stdout ``` Use `pear dump` when you want the exact file bytes; use `pear changelog` when you want it parsed, semver-filtered, and formatted. See also [#see-also] * [`pear changelog` reference](/reference/pear/cli#pear-changelog)—the command and its flags. * [`pear-changelog` module](https://github.com/holepunchto/pear-changelog)—the parser that defines the format, with a `diff()` API for comparing changelogs. * [Ship your app](/getting-started/build-a-peer-to-peer-chat/ship)—the stage and provision flow the changelog travels with. * [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment)—the full operator release flow. * [Configuration](/reference/pear/configuration#pear-stage)—`pear.stage` options that control what gets staged. # Store and replicate import { Cards, Card } from 'fumadocs-ui/components/card' Recipes for persisting data to append-only logs and replicating it across peers. # Replicate and persist with Hypercore {/* */} In this guide you'll extend the ephemeral chat example in [Connect Many Peers](/how-to/connect-to-peers/connect-to-many-peers-by-topic-with-hyperswarm) by using Hypercore to add two significant new features: * **Persistence**: The owner of the Hypercore can add messages at any time, and they'll be persisted to disk. Whenever they come online, readers can replicate these messages over [Hyperswarm](/reference/building-blocks/hyperswarm). * **Many Readers:** New messages added to the Hypercore will be broadcast to interested readers. The owner gives each reader a reading capability ([`core.key`](/reference/building-blocks/hypercore#corekey)) and a corresponding discovery key ([`core.discoveryKey`](/reference/building-blocks/hypercore#corediscoverykey)). The former is used to authorize the reader, ensuring that they have permission to read messages, and the latter is used to discover the owner (and other readers) on the swarm. [`Hypercore`](/reference/building-blocks/hypercore) is a secure, distributed append-only log. It is built for sharing enormous datasets and streams of real-time data. It has a secure transport protocol, making it easy to build fast and scalable peer-to-peer applications. The following example consists of two Pear Terminal Applications: [`writer-app`](#create-the-writer-app) and [`reader-app`](#create-the-reader-app). When these two applications are opened, two peers are created and connected to each other. Hypercore stores the data entered into the command line. {/* Shared snippet (content/_snippets/) included via Fumadocs by how-tos whose Pear-end (worker) logic works with EITHER boilerplate (desktop or terminal). Carries the cross-platform portability point too, so it REPLACES _pear-end-portability-callout on these guides—one orientation callout, not two. For frontend-specific how-tos, use _frontend-template-callout.mdx instead. See: https://www.fumadocs.dev/docs/markdown#include */} **Pear-end logic—start from a boilerplate.** The code in this guide lives in the Bare [worker](/explanation/workers): peer-to-peer logic, no UI. The same worker runs unchanged on desktop, terminal, and mobile—only the shell differs (see [Runtime and languages](/explanation/runtime-and-languages)). Start from a boilerplate in [Start from a template](/getting-started/from-a-template)—desktop ([`hello-pear-electron`](/getting-started/from-a-template/start-from-hello-pear-electron)) or terminal ([`hello-pear-bare`](/getting-started/from-a-template/start-from-hello-pear-bare))—and add this capability on top. Create the writer app [#create-the-writer-app] The `writer-app` stores command-line input to a Hypercore instance and replicates that instance to other peers over [Hyperswarm](/reference/building-blocks/hyperswarm). Create the writer-app directory and add dependencies [#create-the-writer-app-directory-and-add-dependencies] Create the `writer-app` project with these commands: ```sh example=hypercore-replicate step=setup mkdir writer-app cd writer-app npm init -y npm pkg set type="module" npm install bare-path bare-process hypercore hyperswarm b4a ``` This command installs the following dependencies: * [`bare-path`](https://www.npmjs.com/package/bare-path): A module for working with paths. * [`bare-process`](https://www.npmjs.com/package/bare-process): A module for working with processes. * [`hyperswarm`](/reference/building-blocks/hyperswarm): A module for working with Hyperswarm. * [`hypercore`](/reference/building-blocks/hypercore): A module for working with Hypercore. * [`b4a`](https://www.npmjs.com/package/b4a): A module for working with buffers. Add the app logic [#add-the-app-logic] Create the `writer-app/index.js` file with the following content. The writer creates a Hyperswarm and destroys it cleanly on `SIGINT` (L8–L9), then opens a local Hypercore backed by `./storage/writer-storage` (L11). `core.key` and `core.discoveryKey` are only available after [`core.ready()`](/reference/building-blocks/hypercore#await-coreready) resolves, so it awaits that before logging the hex-encoded key readers will need (L14–L15). Every chunk of stdin is appended as its own block (L18). Finally it joins the swarm on the `discoveryKey` and replicates the core to each incoming connection (L22–L23): ```javascript file=/examples/how-to/store-and-replicate/replicate-and-persist-with-hypercore/writer-app/index.js title="writer-app/index.js" lineNumbers {8-9,11,14-15,18,22-23} skip="example-import" import path from 'bare-path' import process from 'bare-process' import Hyperswarm from 'hyperswarm' import Hypercore from 'hypercore' import b4a from 'b4a' const swarm = new Hyperswarm() process.once('SIGINT', () => swarm.destroy().then(() => process.exit(0))) const core = new Hypercore(path.join('./storage', 'writer-storage')) // core.key and core.discoveryKey will only be set after core.ready resolves await core.ready() console.log('hypercore key:', b4a.toString(core.key, 'hex')) // Append all stdin data as separate blocks to the core process.stdin.on('data', (data) => core.append(data)) // core.discoveryKey is *not* a read capability for the core // It's only used to discover other peers who *might* have the core swarm.join(core.discoveryKey) swarm.on('connection', conn => core.replicate(conn)) ``` {/* @harness example=hypercore-replicate step=copy from=examples/how-to/store-and-replicate/replicate-and-persist-with-hypercore/writer-app/index.js to=writer-app/index.js */} Create the reader app [#create-the-reader-app] The `reader-app` uses Hyperswarm to connect to the writer peer and synchronize its local Hypercore with the writer's Hypercore. Create the reader-app directory and add dependencies [#create-the-reader-app-directory-and-add-dependencies] Create the `reader-app` project with these commands: ```sh example=hypercore-replicate step=setup mkdir reader-app cd reader-app npm init -y npm pkg set type="module" npm install bare-path bare-process hypercore hyperswarm ``` This command installs the following dependencies: * [`bare-path`](https://www.npmjs.com/package/bare-path): A module for working with paths. * [`bare-process`](https://www.npmjs.com/package/bare-process): A module for working with processes. * [`hyperswarm`](/reference/building-blocks/hyperswarm): A module for working with Hyperswarm. * [`hypercore`](/reference/building-blocks/hypercore): A module for working with Hypercore. Add the reader-app logic [#add-the-reader-app-logic] Create the `reader-app/index.js` file with the following content. Like the writer, the reader sets up a Hyperswarm with `SIGINT` cleanup (L7–L8). It opens its Hypercore with the writer's key passed on the command line (`Bare.argv[2]`), which makes it a read-only replica, and awaits `core.ready()` (L10–L11). It joins the swarm on the same `discoveryKey` and replicates over each connection (L13–L14), then [`swarm.flush()`](/reference/building-blocks/hyperswarm#await-swarmflush) waits until all discoverable peers are connected (L17) before [`core.update()`](/reference/building-blocks/hypercore#await-coreupdateoptions) pulls the latest length from the writer (L19). It records the current [`core.length`](/reference/building-blocks/hypercore#corelength) and tails the core from that point with a live read stream, logging each new block as it arrives (L21–L24): ```javascript file=/examples/how-to/store-and-replicate/replicate-and-persist-with-hypercore/reader-app/index.js title="reader-app/index.js" lineNumbers {7-8,10-11,13-14,17,19,21-24} skip="example-import" import path from 'bare-path' import process from 'bare-process' import Hyperswarm from 'hyperswarm' import Hypercore from 'hypercore' const swarm = new Hyperswarm() process.once('SIGINT', () => swarm.destroy().then(() => process.exit(0))) const core = new Hypercore(path.join('./storage', 'reader-storage'), Bare.argv[2]) await core.ready() swarm.join(core.discoveryKey) swarm.on('connection', conn => core.replicate(conn)) // swarm.flush() will wait until *all* discoverable peers have been connected to await swarm.flush() await core.update() let position = core.length console.log(`Skipping ${core.length} earlier blocks...`) for await (const block of core.createReadStream({ start: core.length, live: true })) { console.log(`Block ${position++}: ${block}`) } ``` {/* @harness example=hypercore-replicate step=copy from=examples/how-to/store-and-replicate/replicate-and-persist-with-hypercore/reader-app/index.js to=reader-app/index.js */} Run the writer and reader [#run-the-writer-and-reader] In one terminal, run `writer-app` with `bare`: ```sh example=hypercore-replicate step=run process=writer expect="hypercore key:" capture-key="hypercore key: ([0-9a-f]+)" timeout=20000 bare writer-app ``` The `writer-app` will output the [Hypercore](/reference/building-blocks/hypercore) key. In another terminal, open the `reader-app` and pass it the key: ```sh example=hypercore-replicate step=run process=reader cmd="bare reader-app ${key}" expect="Skipping" timeout=45000 bare reader-app ``` {/* @harness example=hypercore-replicate step=send process=writer data="hello\n" delay=2000 */} {/* @harness example=hypercore-replicate step=expect process=reader contains="hello" timeout=30000 */} As inputs are made to the terminal running the writer application, outputs should be shown in the terminal running the reader application. See also [#see-also] * [Work with many Hypercores using Corestore](/how-to/store-and-replicate/work-with-many-hypercores-using-corestore)—recommended pattern when you need more than one core per process. * [From append-only logs to files](/explanation/from-logs-to-files)—how Hypercore, Hyperblobs, and [Hyperdrive](/reference/building-blocks/hyperdrive) relate. * [Share append-only databases with Hyperbee](/how-to/store-and-replicate/share-append-only-databases-with-hyperbee)—key/value store built on Hypercore. * [Storage and distribution](/explanation/storage-and-distribution)—where these cores live on disk. * [Hypercore reference](/reference/building-blocks/hypercore)—full API for the append-only log used in this guide. * [Hyperswarm reference](/reference/building-blocks/hyperswarm)—full API for the topic-based discovery and replication used here. # Share append-only databases with Hyperbee [Hyperbee](/reference/building-blocks/hyperbee) is an append-only B-tree based on [Hypercore](/reference/building-blocks/hypercore). It provides a key/value-store API with methods to insert and get key/value pairs, perform atomic batch insertions, and create sorted iterators. This guide uses [Corestore](/reference/helpers/corestore) and [Hyperswarm](/reference/building-blocks/hyperswarm) to manage and replicate the underlying core; see [Work with many Hypercores using Corestore](/how-to/store-and-replicate/work-with-many-hypercores-using-corestore) if those concepts are unfamiliar. {/* Shared snippet (content/_snippets/) included via Fumadocs by how-tos whose Pear-end (worker) logic works with EITHER boilerplate (desktop or terminal). Carries the cross-platform portability point too, so it REPLACES _pear-end-portability-callout on these guides—one orientation callout, not two. For frontend-specific how-tos, use _frontend-template-callout.mdx instead. See: https://www.fumadocs.dev/docs/markdown#include */} **Pear-end logic—start from a boilerplate.** The code in this guide lives in the Bare [worker](/explanation/workers): peer-to-peer logic, no UI. The same worker runs unchanged on desktop, terminal, and mobile—only the shell differs (see [Runtime and languages](/explanation/runtime-and-languages)). Start from a boilerplate in [Start from a template](/getting-started/from-a-template)—desktop ([`hello-pear-electron`](/getting-started/from-a-template/start-from-hello-pear-electron)) or terminal ([`hello-pear-bare`](/getting-started/from-a-template/start-from-hello-pear-bare))—and add this capability on top. This guide consists of three applications: * [`bee-writer-app`](#create-the-bee-writer-app) - stores 1,000 entries from a given dictionary file into a Hyperbee instance. * [`bee-reader-app`](#create-the-bee-reader-app) - queries the Hyperbee instance for key/value pairs. * [`core-reader-app`](#inspect-the-hyperbee-as-a-hypercore) - inspects the Hyperbee as a Hypercore. Create the bee writer app [#create-the-bee-writer-app] The `bee-writer-app` stores 1,000 entries from a given dictionary file into a Hyperbee instance. The Corestore instance used to create the Hyperbee instance is replicated using Hyperswarm. This enables other peers to replicate their Corestore instance and sparsely (on-demand) download the dictionary data into their local Hyperbee instances. Create the bee-writer-app directory and add dependencies [#create-the-bee-writer-app-directory-and-add-dependencies] Start the `bee-writer-app` project with the following commands: ```sh example=hyperbee-kv step=setup mkdir bee-writer-app cd bee-writer-app npm init -y npm pkg set type="module" npm install corestore hyperswarm hyperbee b4a bare-fs bare-process ``` This will install the following dependencies: * [`bare-fs`](https://www.npmjs.com/package/bare-fs): A module for working with file systems. * [`bare-process`](https://www.npmjs.com/package/bare-process): A module for working with processes. * [`hyperswarm`](/reference/building-blocks/hyperswarm): A module for working with Hyperswarm. * [`corestore`](/reference/helpers/corestore): A module for working with Corestore. * [`hyperbee`](/reference/building-blocks/hyperbee): A module for working with Hyperbee. * [`b4a`](https://www.npmjs.com/package/b4a): A module for working with buffers. Add the bee-writer-app logic [#add-the-bee-writer-app-logic] Create the `bee-writer-app/index.js` file with the following content: A [Corestore](/reference/helpers/corestore) holds the backing core (L8), and a [Hyperswarm](/reference/building-blocks/hyperswarm) replicates it on every connection (L10–L14). The Hyperbee is built on a named core (L17) with UTF-8 key/value encoding (L20–L23). Once the core is ready (L26) it joins the swarm on the core's discovery key (L29), and the writable key is printed only after the topic is announced to the DHT (L32–L34). On the first run ([`core.length <= 1`](/reference/building-blocks/hypercore#corelength)), the dictionary is loaded and inserted in a single atomic batch (L38–L47); on later runs it just re-seeds the existing data (L48–L50). ```javascript file=/examples/how-to/store-and-replicate/share-append-only-databases-with-hyperbee/bee-writer-app/index.js title="bee-writer-app/index.js" lineNumbers {8,10-14,17,20-23,26,29,32-34,38-47} skip="example-import" import fsp from 'bare-fs/promises' import process from 'bare-process' import Hyperswarm from 'hyperswarm' import Corestore from 'corestore' import Hyperbee from 'hyperbee' import b4a from 'b4a' // create a corestore instance with the given location const store = new Corestore('./bee-writer-storage') const swarm = new Hyperswarm() process.once('SIGINT', () => swarm.destroy().then(() => process.exit(0))) // replication of corestore instance swarm.on('connection', conn => store.replicate(conn)) // creation of Hypercore instance (if not already created) const core = store.get({ name: 'my-bee-core' }) // creation of Hyperbee instance using the core instance const bee = new Hyperbee(core, { keyEncoding: 'utf-8', valueEncoding: 'utf-8' }) // wait till all the properties of the hypercore are initialized await core.ready() // join a topic const discovery = swarm.join(core.discoveryKey) // Only display the key once the Hyperbee has been announced to the DHT discovery.flushed().then(() => { console.log('bee key:', b4a.toString(core.key, 'hex')) }) // Only import the dictionary the first time this script is executed // The first block will always be the Hyperbee header block if (core.length <= 1) { console.log('importing dictionary...') const dict = JSON.parse( await fsp.readFile(new URL('./dict.json', import.meta.url)) ) const batch = bee.batch() for (const { key, value } of dict) { await batch.put(key, value) } await batch.flush() } else { // Otherwise just seed the previously-imported dictionary console.log('seeding dictionary...') } ``` Save the dict.json file [#save-the-dictjson-file] Save the `dict.json` file to the `bee-writer-app` directory. The `dict.json` file contains 1,000 dictionary words. This file holds the data that will be stored in the Hyperbee instance, and will be imported into the Hyperbee instance if it is the first time the script is run. [Click here to save `dict.json`](/dict.json). {/* @harness example=hyperbee-kv step=copy from=examples/how-to/store-and-replicate/share-append-only-databases-with-hyperbee/bee-writer-app/index.js to=bee-writer-app/index.js */} {/* @harness example=hyperbee-kv step=copy from=public/dict.json to=bee-writer-app/dict.json */} Run the bee-writer-app [#run-the-bee-writer-app] In one terminal, run `bee-writer-app` with `bare`. ```sh example=hyperbee-kv step=run process=writer expect="bee key:" capture-key="bee key: ([0-9a-f]+)" timeout=60000 bare bee-writer-app ``` Create the bee reader app [#create-the-bee-reader-app] The `bee-reader-app` creates a `Corestore` instance and replicates it using the `Hyperswarm` instance to the same topic as `bee-writer-app`. On every word entered in the command line, it will download the respective data to the local `Hyperbee` instance. Create the bee-reader-app directory and add dependencies [#create-the-bee-reader-app-directory-and-add-dependencies] Create the `bee-reader-app` project with the following commands: ```sh example=hyperbee-kv step=setup mkdir bee-reader-app cd bee-reader-app npm init -y npm pkg set type="module" npm install corestore hyperswarm hyperbee b4a bare-pipe bare-process ``` This will install the following dependencies: * [`bare-process`](https://www.npmjs.com/package/bare-process): A module for working with processes. * [`hyperswarm`](/reference/building-blocks/hyperswarm): A module for working with Hyperswarm. * [`corestore`](/reference/helpers/corestore): A module for working with Corestore. * [`hyperbee`](/reference/building-blocks/hyperbee): A module for working with Hyperbee. * [`b4a`](https://www.npmjs.com/package/b4a): A module for working with buffers. Add the bee-reader-app logic [#add-the-bee-reader-app-logic] Create the `bee-reader-app/index.js` file with the following content: The reader takes the writer's public key as a command-line argument (L8–L10) and opens that core through its own [Corestore](/reference/helpers/corestore) (L13), replicated over [Hyperswarm](/reference/building-blocks/hyperswarm) (L15–L19). The core is reopened from the supplied key (L22) and wrapped in a Hyperbee with matching UTF-8 encoding (L25–L28). After `ready()` (L31) it joins the same topic (L37) and reads words from stdin (L39): each query calls `bee.get(word)` and prints the value, downloading only the blocks needed to satisfy it (L41–L48). ```javascript file=/examples/how-to/store-and-replicate/share-append-only-databases-with-hyperbee/bee-reader-app/index.js title="bee-reader-app/index.js" lineNumbers {8-10,13,15-19,22,25-28,31,37,39,41-48} skip="example-import" import process from 'bare-process' import Hyperswarm from 'hyperswarm' import Corestore from 'corestore' import Hyperbee from 'hyperbee' import Pipe from 'bare-pipe' import b4a from 'b4a' const key = Bare.argv[2] if (!key) throw new Error('provide a key') // creation of a corestore instance const store = new Corestore('./bee-reader-storage') const swarm = new Hyperswarm() process.once('SIGINT', () => swarm.destroy().then(() => process.exit(0))) // replication of the corestore instance on connection with other peers swarm.on('connection', (conn) => store.replicate(conn)) // create or get the hypercore using the public key supplied as command-line argument const core = store.get({ key: b4a.from(key, 'hex') }) // create a hyperbee instance using the hypercore instance const bee = new Hyperbee(core, { keyEncoding: 'utf-8', valueEncoding: 'utf-8' }) // wait till the hypercore properties to be initialized await core.ready() // logging the public key of the hypercore instance console.log('core key here is:', core.key.toString('hex')) // Attempt to connect to peers swarm.join(core.discoveryKey) const stdin = new Pipe(0) stdin.on('data', (data) => { const word = data.toString().trim() if (!word.length) return bee.get(word).then(node => { if (!node || !node.value) console.log(`No dictionary entry for ${word}`) else console.log(`${word} -> ${node.value}`) setImmediate(console.log) // flush hack }, console.error) }) ``` {/* @harness example=hyperbee-kv step=copy from=examples/how-to/store-and-replicate/share-append-only-databases-with-hyperbee/bee-reader-app/index.js to=bee-reader-app/index.js */} Run the bee-reader-app [#run-the-bee-reader-app] In another terminal, run the `bee-reader-app` with `bare` and pass it the core key from the [`bee-writer-app`](#run-the-bee-writer-app). ```sh example=hyperbee-kv step=run process=reader cmd="bare bee-reader-app ${key}" expect="core key here is:" timeout=45000 bare bee-reader-app ``` {/* @harness example=hyperbee-kv step=send process=reader data="hello\n" delay=3000 */} {/* @harness example=hyperbee-kv step=expect process=reader contains="hello" timeout=30000 */} Query the database by entering a key from the [`dict file`](#save-the-dictjson-file) to lookup into the `bee-reader-app` terminal and hitting return. Each application has its own Corestore directory: `./bee-reader-storage` for the reader and `./bee-writer-storage` for the writer. Look at the disk space for the `bee-reader-app` storage path after each query and notice that it's significantly smaller than `bee-writer-app`. This is because Hyperbee only downloads the [Hypercore](/reference/building-blocks/hypercore) blocks it needs to satisfy each query, a feature called **sparse downloading**. A Hyperbee is itself a Hypercore: its B-tree nodes are stored as Hypercore blocks. That's what makes the next section possible—inspecting the same data as a raw Hypercore. Inspect the Hyperbee as a Hypercore [#inspect-the-hyperbee-as-a-hypercore] Create the core-reader-app directory and add dependencies [#create-the-core-reader-app-directory-and-add-dependencies] Finally create the `core-reader-app` project with the following commands: ```sh skip="manual-walkthrough" mkdir core-reader-app cd core-reader-app npm init -y npm pkg set type="module" npm install corestore hyperswarm hyperbee b4a bare-process ``` This will install the following dependencies: * [`bare-process`](https://www.npmjs.com/package/bare-process): A module for working with processes. * [`hyperswarm`](/reference/building-blocks/hyperswarm): A module for working with Hyperswarm. * [`corestore`](/reference/helpers/corestore): A module for working with Corestore. * [`hyperbee`](/reference/building-blocks/hyperbee): A module for working with Hyperbee. * [`b4a`](https://www.npmjs.com/package/b4a): A module for working with buffers. Add the core-reader-app logic [#add-the-core-reader-app-logic] Create the `core-reader-app/index.js` file with the following content: This app treats the Hyperbee purely as a [Hypercore](/reference/building-blocks/hypercore). It imports Hyperbee's `Node` encoding directly so it can decode tree nodes (L6), takes the writer's key as an argument (L8–L10), and opens the core through a [Corestore](/reference/helpers/corestore) (L13) replicated over [Hyperswarm](/reference/building-blocks/hyperswarm) (L15–L19). The core is reopened from the key and made ready (L22–L24), then joins the topic and waits for the swarm to flush (L27–L28). After pulling the latest metadata with [`core.update()`](/reference/building-blocks/hypercore#await-coreupdateoptions) (L31), it fetches the last block by sequence number (L33–L34) and logs it both raw and decoded through `Node.decode` (L37–L38). ```javascript file=/examples/how-to/store-and-replicate/share-append-only-databases-with-hyperbee/core-reader-app/index.js title="core-reader-app/index.js" lineNumbers {6,8-10,13,15-19,22-24,27-28,31,33-34,37-38} skip="example-import" import process from 'bare-process' import Hyperswarm from 'hyperswarm' import Corestore from 'corestore' import b4a from 'b4a' import { Node } from 'hyperbee/lib/messages.js' const key = Bare.argv[2] if (!key) throw new Error('provide a key') // creation of a corestore instance const store = new Corestore('./reader-storage') const swarm = new Hyperswarm() process.once('SIGINT', () => swarm.destroy().then(() => process.exit(0))) // replication of the corestore instance on connection with other peers swarm.on('connection', conn => store.replicate(conn)) // create or get the hypercore using the public key supplied as command-line argument const core = store.get({ key: b4a.from(key, 'hex') }) // wait till the properties of the hypercore instance are initialized await core.ready() // join a topic swarm.join(core.discoveryKey) await swarm.flush() // update the meta-data information of the hypercore instance await core.update() const seq = core.length - 1 const lastBlock = await core.get(core.length - 1) // print the information about the last block or the latest block of the hypercore instance console.log(`Raw Block ${seq}:`, lastBlock) console.log(`Decoded Block ${seq}`, Node.decode(lastBlock)) ``` Run the core-reader-app [#run-the-core-reader-app] In another terminal, run the `core-reader-app` with `bare`, passing the core key from the [`bee-writer-app`](#run-the-bee-writer-app): ```sh skip="manual-walkthrough" bare core-reader-app ``` You can now examine the Hyperbee as if it were just a Hypercore. The `core-reader-app` will continually download and log the last block of the Hypercore containing the Hyperbee data. Note that these blocks are encoded using Hyperbee's `Node` encoding, which has been imported directly from `Hyperbee` for the purposes of explanation. See also [#see-also] * [Create a full peer-to-peer filesystem with Hyperdrive](/how-to/stream-and-share-media/create-a-full-peer-to-peer-filesystem-with-hyperdrive)—a richer data structure built on the same Hyperbee primitive. * [Work with many Hypercores using Corestore](/how-to/store-and-replicate/work-with-many-hypercores-using-corestore)—the Corestore patterns this guide builds on. * [Hyperbee reference](/reference/building-blocks/hyperbee)—full API. * [Corestore reference](/reference/helpers/corestore)—full API for the store used to manage and replicate the backing core. # Work with many Hypercores using Corestore An append-only log like Hypercore is powerful on its own, but it's most useful as a building-block for constructing larger data structures, such as databases or filesystems. Building these data structures often requires many cores, each with different responsibilities. For example, [Hyperdrive](/reference/building-blocks/hyperdrive) uses one core to store file metadata and another to store file contents. [`Corestore`](/reference/helpers/corestore) is a [Hypercore](/reference/building-blocks/hypercore) factory that makes it easier to manage large collections of named Hypercores. This guide demonstrates a pattern often in use: co-replicating many cores using Corestore, where several 'internal cores' are linked to from a primary core. Only the primary core is announced on the swarm—the keys for the others are recorded inside that core. In [Replicate and persist with Hypercore](/how-to/store-and-replicate/replicate-and-persist-with-hypercore), only a single Hypercore instance was replicated. But in this guide, you'll replicate a single Corestore instance, which will internally manage the replication of a collection of Hypercores. You'll achieve this with two Pear Terminal Applications: [`multicore-writer-app`](#create-the-multicore-writer-app) and [`multicore-reader-app`](#create-the-multicore-reader-app). {/* Shared snippet (content/_snippets/) included via Fumadocs by the chat-app delta how-tos. Reminds readers these guides teach portable Pear-end (worker) logic, not desktop-specific code. See: https://www.fumadocs.dev/docs/markdown#include */} **This guide is about the Pear-end, not the shell.** The code below lives in the Bare [worker](/explanation/workers)—the peer-to-peer logic, not the user interface. Because the Pear-end never imports DOM APIs and never assumes a UI framework, the same worker is portable across **desktop (Electron)**, **mobile (React Native via Bare iOS / Bare Android)**, and **terminal**. The example apps ship an Electron shell, but only the UI half changes per platform—the logic here stays the same. See [Runtime and languages](/explanation/runtime-and-languages) for the cross-platform model and current support. **Use one Corestore instance per application.** Multiple Corestores over the same storage cause file-locking errors and duplicate core storage. A single Corestore: * Reduces open file handles. * Reduces storage by deduping Hypercore storage. * Requires only one replication stream per peer connection. * Simplifies referring to Hypercores by name. If named cores collide across components, namespace them ([`store.namespace('a')`](/reference/helpers/corestore#storenamespacename))—retrieving cores by `key` is unaffected by namespacing. Create the multicore writer app [#create-the-multicore-writer-app] Create the multicore writer app directory and add dependencies [#create-the-multicore-writer-app-directory-and-add-dependencies] Create the `multicore-writer-app` project with these commands: ```sh example=corestore-multi step=setup mkdir multicore-writer-app cd multicore-writer-app npm init -y npm pkg set type="module" npm install bare-process corestore hyperswarm b4a ``` This will install the following dependencies: * [`bare-process`](https://www.npmjs.com/package/bare-process): A module for working with processes. * [`corestore`](/reference/helpers/corestore): A module for working with Corestore. * [`hyperswarm`](/reference/building-blocks/hyperswarm): A module for working with Hyperswarm. * [`b4a`](https://www.npmjs.com/package/b4a): A module for working with buffers. Add the multicore writer app logic [#add-the-multicore-writer-app-logic] Create the `multicore-writer-app/index.js` file with the following content: The `multicore-writer-app` uses a Corestore instance to create three Hypercores, which are then replicated with other peers using `Hyperswarm`: * One Corestore and one Hyperswarm back the whole app (L7–L8). `core1` bootstraps the system—its first block records the keys of `core2` and `core3` (L22–L26), so a reader only needs `core1`'s key to discover the rest. Writing the bootstrap list before announcing on the swarm ensures `core1.get(0)` resolves as soon as a reader connects. * The three named cores are created up front (L13–L15); names map to local key pairs and are never sent to readers. * Only `core1`'s discovery key is announced on the swarm (L32)—`core2` and `core3` ride along because [`store.replicate(conn)`](/reference/helpers/corestore#storereplicateoptsorstream) replicates every loaded core over a single stream (L36). * The main core key is logged so it can be passed to the [`multicore-reader-app`](#create-the-multicore-reader-app) (L28). * Terminal input is routed by length: short messages append to `core2`, long ones to `core3` (L39–L46). ```javascript file=/examples/how-to/store-and-replicate/work-with-many-hypercores-using-corestore/multicore-writer-app/index.js title="multicore-writer-app/index.js" lineNumbers {7-8,13-15,22-26,28,32,36,39-46} skip="example-import" import Hyperswarm from 'hyperswarm' import Corestore from 'corestore' import b4a from 'b4a' import process from 'bare-process' const store = new Corestore('./multicore-writer-storage') const swarm = new Hyperswarm() process.once('SIGINT', () => swarm.destroy().then(() => process.exit(0))) // A name is a purely-local, and maps to a key pair. It's not visible to readers. // Since a name always corresponds to a key pair, these are all writable const core1 = store.get({ name: 'core-1', valueEncoding: 'json' }) const core2 = store.get({ name: 'core-2' }) const core3 = store.get({ name: 'core-3' }) await Promise.all([core1.ready(), core2.ready(), core3.ready()]) // Since Corestore does not exchange keys, they need to be exchanged elsewhere. // Here, we'll record the other keys in the first block of core1. Do this // *before* announcing on the swarm so that as soon as a reader connects, // `core1.get(0)` resolves and the bootstrap key list is available. if (core1.length === 0) { await core1.append({ otherKeys: [core2, core3].map((core) => b4a.toString(core.key, 'hex')) }) } console.log('main core key:', b4a.toString(core1.key, 'hex')) // Here we'll only join the swarm with the core1's discovery key // We don't need to announce core2 and core3, because they'll be replicated with core1 swarm.join(core1.discoveryKey) // Corestore replication internally manages to replicate every loaded core // Corestore *does not* exchange keys (read capabilities) during replication. swarm.on('connection', (conn) => store.replicate(conn)) // Record all short messages in core2, and all long ones in core3 process.stdin.on('data', (data) => { if (data.length < 5) { console.log('appending short data to core2') core2.append(data) } else { console.log('appending long data to core3') core3.append(data) } }) ``` {/* @harness example=corestore-multi step=copy from=examples/how-to/store-and-replicate/work-with-many-hypercores-using-corestore/multicore-writer-app/index.js to=multicore-writer-app/index.js */} Create the multicore reader app [#create-the-multicore-reader-app] The `multicore-reader-app` connects to the previous peer with `Hyperswarm` and replicates the local `Corestore` instance to receive the data from it. This requires the copied key to be supplied as an argument when executing the file, which will then be used to create a core with the same public key as the other peer (that is, the same discovery key for both the reader and writer peers). Create the multicore reader app directory and add dependencies [#create-the-multicore-reader-app-directory-and-add-dependencies] Create the `multicore-reader-app` project with these commands: ```sh example=corestore-multi step=setup mkdir multicore-reader-app cd multicore-reader-app npm init -y npm pkg set type="module" npm install corestore hyperswarm b4a bare-process ``` This will install the following dependencies: * [`bare-process`](https://www.npmjs.com/package/bare-process): A module for working with processes. * [`corestore`](/reference/helpers/corestore): A module for working with Corestore. * [`hyperswarm`](/reference/building-blocks/hyperswarm): A module for working with Hyperswarm. * [`b4a`](https://www.npmjs.com/package/b4a): A module for working with buffers. Add the multicore reader app logic [#add-the-multicore-reader-app-logic] Create the `multicore-reader-app/index.js` file with the following content. The reader takes the writer's main core key as a command-line argument (L6–L8) and opens its own Corestore (L10–L11). On each connection it replicates the whole store (L17), then gets `core1` from the supplied key, joins the swarm on its discovery key, and flushes discovery (L20–L25). [`core.get(0)`](/reference/building-blocks/hypercore#await-coregetindex-options) then blocks until the writer's first block—the bootstrap key list—has actually replicated; unlike [`core.update()`](/reference/building-blocks/hypercore#await-coreupdateoptions) it waits for the data itself, and a timeout surfaces a clear error if the writer is never reachable instead of reading an empty core or hanging (L27–L36). It reads the bootstrap key list from that block (L39) and, for every key, gets the corresponding core and logs each new block as it is appended (L40–L50): ```javascript file=/examples/how-to/store-and-replicate/work-with-many-hypercores-using-corestore/multicore-reader-app/index.js title="multicore-reader-app/index.js" lineNumbers {6-8,10-11,17,20-25,27-36,39,40-50} skip="example-import" import process from 'bare-process' import Corestore from 'corestore' import Hyperswarm from 'hyperswarm' import b4a from 'b4a' if (!Bare.argv[2]) throw new Error('provide a key') const key = b4a.from(Bare.argv[2], 'hex') const store = new Corestore('./multicore-reader-storage') await store.ready() const swarm = new Hyperswarm() process.once('SIGINT', () => swarm.destroy().then(() => process.exit(0))) // replication of corestore instance on every connection swarm.on('connection', (conn) => store.replicate(conn)) // creation/getting of a hypercore instance using the key passed const core = store.get({ key, valueEncoding: 'json' }) // wait till all the properties of the hypercore instance are initialized await core.ready() swarm.join(core.discoveryKey) await swarm.flush() // core.get(0) blocks until the writer's first block (the bootstrap key list) has // replicated from a connected peer — unlike core.update(), it waits for the data // itself rather than just peer discovery. The timeout surfaces a clear error if // the writer is never reachable, instead of leaving the reader hanging forever. let firstBlock try { firstBlock = await core.get(0, { timeout: 30000 }) } catch { throw new Error('Could not connect to the writer peer') } // read the bootstrap key list (the other core keys) from the first block const { otherKeys } = firstBlock for (const key of otherKeys) { const core = store.get({ key: b4a.from(key, 'hex') }) // on every append to the hypercore, // download the latest block of the core and log it to the console core.on('append', () => { const seq = core.length - 1 core.get(seq).then(block => { console.log(`Block ${seq} in Core ${key}: ${block}`) }) }) } ``` {/* @harness example=corestore-multi step=copy from=examples/how-to/store-and-replicate/work-with-many-hypercores-using-corestore/multicore-reader-app/index.js to=multicore-reader-app/index.js */} Run the writer and reader [#run-the-writer-and-reader] Run the multicore writer app [#run-the-multicore-writer-app] In one terminal, run `multicore-writer-app` with `bare`. ```sh example=corestore-multi step=run process=writer expect="main core key:" capture-key="main core key: ([0-9a-f]+)" timeout=20000 bare multicore-writer-app ``` The `multicore-writer-app` will output the main core key. Run the multicore reader app [#run-the-multicore-reader-app] In another terminal, open the `multicore-reader-app` and pass it the key: ```sh example=corestore-multi step=run process=reader cmd="bare multicore-reader-app ${key}" expect-alive=8000 timeout=45000 bare multicore-reader-app ``` {/* @harness example=corestore-multi step=send process=writer data="this is a longer message\n" delay=2000 */} {/* @harness example=corestore-multi step=expect process=reader contains="Block 0 in Core" timeout=30000 */} As inputs are made to the terminal running the writer application, outputs should be shown in the terminal running the reader application. See also [#see-also] * [Share append-only databases with Hyperbee](/how-to/store-and-replicate/share-append-only-databases-with-hyperbee)—key/value store on top of Hypercore. * [Create a full peer-to-peer filesystem with Hyperdrive](/how-to/stream-and-share-media/create-a-full-peer-to-peer-filesystem-with-hyperdrive)—filesystem on top of two Hypercores. * [Host multiple rooms in one chat app](/how-to/connect-to-peers/host-multiple-rooms-in-one-chat-app)—this co-replication pattern applied in a full app, where one Corestore backs many rooms. * [Corestore reference](/reference/helpers/corestore)—full API for the store factory, namespacing, and replication surface used here. * [Hypercore reference](/reference/building-blocks/hypercore)—full API for the individual append-only log each Corestore session manages. # Bundle a Bare app A Bare program ships in one of two shapes depending on where it runs: as an **embeddable bundle** loaded by a [worklet](/how-to/run-on-native/embed-bare-in-react-native) inside a native app, or as a **standalone executable** with no peer dependencies. This guide covers both, plus where native addons fit in. Embeddable bundle with bare-pack [#embeddable-bundle-with-bare-pack] [`bare-pack`](/reference/modules/bare-modules) traverses your module graph and produces a single [`bare-bundle`](https://github.com/holepunchto/bare-bundle) with import specifiers pre-resolved and addons and assets embedded. Build one for the platform you're embedding into: ```sh skip="install-instruction" npm i -g bare-pack ``` ```console skip="native-build" bare-pack --linked --host ios --out app.bundle.mjs app.js ``` Two flags matter for mobile: `--host [-[-]]` targets a specific system (pass it more than once for a combined bundle), and `--linked` makes addons resolve to `linked:` specifiers—required on iOS and Android, which link native code ahead of time rather than loading it from disk at runtime. Load the result in a worklet by giving `start()` a filename with the `.bundle` extension: ```js skip="snippet-illustration" import { Worklet } from 'react-native-bare-kit' import bundle from './app.bundle.mjs' const worklet = new Worklet() worklet.start('/app.bundle', bundle) ``` Standalone executable with bare-build [#standalone-executable-with-bare-build] [`bare-build`](https://github.com/holepunchto/bare-build) packages your code as a native application bundle or a standalone executable for desktop and mobile. It ships portable runtimes for every supported system, so the output runs with no Node.js, Bare, or [Pear CLI](/reference/pear/cli) installed: ```sh skip="install-instruction" npm i -g bare-build ``` ```console skip="native-build" bare-build \ --host darwin-arm64 --host darwin-x64 \ --standalone \ --identifier com.example.App \ app.js ``` The output format depends on platform and mode—`--standalone` emits a self-extracting executable (Mach-O on macOS/iOS, ELF on Linux/Android, PE on Windows), while `--package` emits an installer (`.pkg`, `.AppImage`, `.msix`, …). The portable runtimes run only Bare's I/O event loop, which suits headless CLIs and services. A native GUI app needs tight integration with the system event loop—pass a native runtime such as `--runtime bare-app-kit/runtime` (macOS) or `--runtime bare-ndk/runtime` (Android) instead. This is the path the [`hello-pear-bare`](/getting-started/from-a-template/start-from-hello-pear-bare) template wires behind `npm run make`, which detects your host and builds the matching target. Native addons and prebuilt runtimes [#native-addons-and-prebuilt-runtimes] Native addons are compiled separately with [`bare-make`](https://github.com/holepunchto/bare-make) (a CMake-based generator using Ninja + Clang); `bare-pack` then embeds the results. To build addons for every platform you ship to, the [Bare native-addon prebuild actions](/reference/ci-and-release/github-actions#bare-native-addon-prebuilds) run `bare-make` across a CI matrix. If you only need the runtime itself, [`bare-runtime`](https://github.com/holepunchto/bare-runtime) provides prebuilt Bare binaries for macOS, iOS, Linux, Android, and Windows. See also [#see-also] * [Embed Bare in a React Native app](/how-to/run-on-native/embed-bare-in-react-native)—load the bundle in a worklet. * [Start from the hello-pear-bare template](/getting-started/from-a-template/start-from-hello-pear-bare)—a terminal app that builds standalone with `bare-build`. * [Bare modules](/reference/modules/bare-modules)—`bare-pack`, `bare-bundle`, `bare-make`, and the rest of the build tooling. * [One core, many platforms](/explanation/bare-on-native)—why the embeddable-bundle path exists. # Embed Bare in a React Native app import { Steps, Step } from 'fumadocs-ui/components/steps' This guide runs a [Bare](/explanation/bare-runtime) core inside a React Native app using [`react-native-bare-kit`](/reference/bare/bare-kit), and wires a two-way message channel between the UI and the core. This is the practical version of the pattern described in [One core, many platforms](/explanation/bare-on-native): your peer-to-peer logic lives in the worklet, your UI stays native. You'll need an existing React Native or Expo project to add this to. Install the kit [#install-the-kit] ```sh skip="install-instruction" npm i react-native-bare-kit b4a ``` For Expo, follow the [`bare-expo`](https://github.com/holepunchto/bare-expo) example, which shows the config-plugin setup; the worklet code below is identical. Write the Bare core [#write-the-bare-core] The worklet entry script runs on its own thread. It reaches the host through the `BareKit.IPC` channel—an instance of [`Bare.IPC`](/reference/bare/runtime#bareipc). Here it echoes back whatever it receives: ```js skip="snippet-illustration" // app.js—runs inside the worklet const { IPC } = BareKit IPC.on('data', (data) => { const message = data.toString() IPC.write(Buffer.from(`echo: ${message}`)) }) ``` This is ordinary Bare code: it can `require` any [`bare-*` module](/reference/modules/bare-modules) and open a [Hyperswarm](/reference/building-blocks/hyperswarm) or [Corestore](/reference/helpers/corestore) just as it would on the desktop. Start the worklet from React Native [#start-the-worklet-from-react-native] In your React Native component—for example `App.tsx`, not the worklet's `/app.js`—create a `Worklet`, start it with the core's source, and read and write the `IPC` object it exposes: ```js skip="snippet-illustration" import { Worklet } from 'react-native-bare-kit' import b4a from 'b4a' const source = ` const { IPC } = BareKit IPC.on('data', (data) => IPC.write(Buffer.from('echo: ' + data.toString()))) ` const worklet = new Worklet() worklet.start('/app.js', source) const { IPC } = worklet IPC.on('data', (data) => console.log(b4a.toString(data))) IPC.write(b4a.from('Hello from React Native!')) ``` The worklet honours the Bare [lifecycle](/reference/bare/runtime#lifecycle), so call `worklet.suspend()` and `worklet.resume()` from your app's background/foreground handlers to keep the core in step with the OS. Run it [#run-it] Build and run on a simulator or device with your usual React Native toolchain. Worklet `console.*` output is written to the system log under the `bare` identifier—view it with the platform's native logging tools (Console.app on iOS, `logcat` on Android). ```text skip="sample-output" echo: Hello from React Native! ``` The worklet must stop all active I/O when backgrounded or the OS will force-terminate it. See [Handle app suspension](/how-to/run-on-native/handle-app-suspension) for patterns covering TCP servers, Hyperswarm, timers, and IPC handles. Next steps [#next-steps] * Inlining source is fine for a snippet, but real apps ship a prebuilt bundle. See [Bundle a Bare app](/how-to/run-on-native/bundle-a-bare-app) to produce a `.bundle` and load it with `worklet.start('/app.bundle', bundle)`. * Passing raw bytes gets unwieldy fast. See [Type a native RPC bridge](/how-to/run-on-native/type-a-native-rpc-bridge) to put typed, schema-generated methods on top of this IPC channel. * For the full Worklet and IPC API, see the [`bare-kit` reference](/reference/bare/bare-kit). # Handle app suspension import { Callout } from 'fumadocs-ui/components/callout' Getting suspension wrong is silent: the OS gives the app no warning before it force-terminates it. A backgrounded app that leaves an HTTP server listening, a TCP socket open, or a live timer running will be killed—usually within a few seconds on iOS and Android. Mobile operating systems suspend apps when they move to the background. Bare models this explicitly through its [lifecycle](/reference/bare/runtime#lifecycle): 1. the host calls `Bare.suspend()`, which emits a `suspend` event so your code can stop outstanding work, 2. then the loop drains and emits `idle`, 3. and finally blocks—keeping the process alive but quiet. 4. `Bare.resume()` brings it back. The problem is step 2. The loop goes `idle` only when it has **no remaining referenced handles**: no open sockets, no listening servers, no active timers, no pending file operations. If any referenced handle survives the `suspend` event, the loop never emits `idle`, the process never fully suspends, and the OS terminates it without warning when its patience runs out. For the state machine behind this, see [Inside Bare](/explanation/bare-runtime#the-lifecycle). The two patterns [#the-two-patterns] Every handle you manage falls into one of two categories. Pattern A — unref() for handles that survive suspension [#pattern-a--unref-for-handles-that-survive-suspension] Some handles must stay open across a suspend/resume cycle—for example, the IPC channel between a [bare-kit worklet](/reference/bare/bare-kit) and its host. Calling [`.unref()`](/reference/bare/modules/bare-ipc#unref-this) removes the handle from the loop's reference count without closing it, so the loop can go idle while the handle stays alive. Call [`.ref()`](/reference/bare/modules/bare-ipc#ref-this) on `resume` to recount it. ```js skip="snippet-illustration" const IPC = require('bare-ipc') const [portA] = IPC.open() const ipc = portA.connect() Bare.on('suspend', () => ipc.unref()) Bare.on('resume', () => ipc.ref()) ``` Pattern B—close on suspend, recreate on resume [#pattern-bclose-on-suspend-recreate-on-resume] Most active I/O should not survive suspension. Stop it on `suspend` and start it again on `resume`. The OS connection timeout is shorter than you might expect—don't rely on connections surviving while the process is idle. Examples [#examples] Stop a bare-tcp server [#stop-a-bare-tcp-server] `server.close()` stops accepting new connections, but existing connections are still referenced. Destroy them first, or the loop will not go idle. ```js skip="snippet-illustration" const net = require('bare-tcp') let server = null function startServer () { server = net.createServer((socket) => { socket.on('data', (data) => socket.write(data)) // echo }) server.listen(3000) } startServer() Bare.on('suspend', () => { for (const socket of server.connections) socket.destroy() server.close() server = null }) Bare.on('resume', () => startServer()) ``` See [`bare-tcp`](/reference/bare/modules/bare-tcp) for the full server and socket API. Suspend Hyperswarm [#suspend-hyperswarm] Each `connection` event gives you a live socket stream that Hyperswarm tracks internally. Rather than tearing the swarm down, call [`swarm.suspend()`](/reference/building-blocks/hyperswarm#await-swarmsuspend-log---) to pause its connections, listening server, and discovery, and [`swarm.resume()`](/reference/building-blocks/hyperswarm#await-swarmresume-log---) to bring them back—the swarm instance and its joined topics survive the cycle. ```js skip="snippet-illustration" const Hyperswarm = require('hyperswarm') const Corestore = require('corestore') const store = new Corestore('./data') const topic = Buffer.alloc(32) // your 32-byte topic const swarm = new Hyperswarm() swarm.on('connection', (conn) => store.replicate(conn)) swarm.join(topic) Bare.on('suspend', async () => { await swarm.suspend() // pause connections, server, and discovery await store.suspend() // flush buffered writes to disk }) Bare.on('resume', async () => { await store.resume() await swarm.resume() // reconnect and rejoin discovery }) ``` Suspending the swarm stops the replication streams and DHT socket—the handles it holds in the loop—while keeping the swarm and its joined topics, so `resume` reconnects without rejoining. The `store` does not keep the loop referenced while idle, but it may hold buffered writes that have not yet reached disk. Call [`store.suspend()`](/reference/helpers/corestore#await-storesuspendoptions) to flush them before the process suspends, and [`store.resume()`](/reference/helpers/corestore#await-storeresume) on the way back—if the OS force-terminates the app before a flush, recent writes can be lost. See [Hyperswarm](/reference/building-blocks/hyperswarm) and [Corestore](/reference/helpers/corestore). Unref the bare-kit IPC channel [#unref-the-bare-kit-ipc-channel] The IPC channel between a worklet and its native host must survive suspension so the host can still send messages (for example, a `resume` instruction). Use [`.unref()`](/reference/bare/modules/bare-ipc#unref-this) / [`.ref()`](/reference/bare/modules/bare-ipc#ref-this) (Pattern A): ```js skip="snippet-illustration" // worklet entry — runs inside Bare const { IPC } = BareKit Bare.on('suspend', () => IPC.unref()) Bare.on('resume', () => IPC.ref()) IPC.on('data', (data) => { IPC.write(Buffer.from(`echo: ${data}`)) }) ``` See [`bare-ipc`](/reference/bare/modules/bare-ipc) for the [`.ref()`](/reference/bare/modules/bare-ipc#ref-this) / [`.unref()`](/reference/bare/modules/bare-ipc#unref-this) API, and [`bare-kit`](/reference/bare/bare-kit) for the host-side worklet API. Clear timers [#clear-timers] Active [`setInterval`](/reference/bare/modules/bare-timers#setinterval) and [`setTimeout`](/reference/bare/modules/bare-timers#settimeout) callbacks hold a reference in the event loop just like open sockets. Clear them on `suspend` and restart on `resume`. ```js skip="snippet-illustration" let heartbeat = null function startHeartbeat () { heartbeat = setInterval(() => sync(), 30_000) } startHeartbeat() Bare.on('suspend', () => { clearInterval(heartbeat) heartbeat = null }) Bare.on('resume', () => startHeartbeat()) ``` See [`bare-timers`](/reference/bare/modules/bare-timers) for the full timer API, including [`.ref()`](/reference/bare/modules/bare-timers#task) / [`.unref()`](/reference/bare/modules/bare-timers#task) for a timer that must keep running across the cycle. How the host triggers suspension [#how-the-host-triggers-suspension] On mobile, the embedder calls [`bare_suspend()`](/reference/bare/runtime#lifecycle) and [`bare_resume()`](/reference/bare/runtime#lifecycle) from the C API when the OS fires its own app-lifecycle callbacks. [`react-native-bare-kit`](https://github.com/holepunchto/react-native-bare-kit) subscribes to React Native [`AppState`](https://reactnative.dev/docs/appstate) changes and calls `worklet.suspend()` and `worklet.resume()` on your behalf. You do not need to wire this up yourself; adding your own listeners is redundant but harmless. If you need explicit control—for example, to coordinate suspension with custom host logic—you can drive the worklet directly: ```js skip="snippet-illustration" // React Native host component import { AppState } from 'react-native' import { Worklet } from 'react-native-bare-kit' const worklet = new Worklet() worklet.start('/app.js', source) AppState.addEventListener('change', (state) => { if (state === 'background') worklet.suspend() else if (state === 'active') worklet.resume() }) ``` Whether suspension is triggered by `react-native-bare-kit` or from your own host code, the JavaScript inside the worklet responds to the resulting [`suspend`](/reference/bare/runtime#events) and [`resume`](/reference/bare/runtime#events) events via `Bare.on('suspend')` and `Bare.on('resume')`. See [Embed Bare in a React Native app](/how-to/run-on-native/embed-bare-in-react-native) for the full worklet setup. *** See also [#see-also] * [Inside Bare](/explanation/bare-runtime#the-lifecycle)—why the lifecycle model exists and the state machine behind it. * [Bare runtime API](/reference/bare/runtime#lifecycle)—the full `suspend`, `wakeup`, `idle`, and `resume` event reference. * [One core, many platforms](/explanation/bare-on-native)—embedding Bare in a native app. * [Embed Bare in a React Native app](/how-to/run-on-native/embed-bare-in-react-native)—the worklet and IPC setup this page builds on. * [`bare-kit` reference](/reference/bare/bare-kit)—worklet API and host IPC channel. * [`bare-tcp`](/reference/bare/modules/bare-tcp)—TCP server and socket. * [`bare-ipc`](/reference/bare/modules/bare-ipc)—IPC streams and `ref()` / `unref()`. * [`bare-timers`](/reference/bare/modules/bare-timers)—timer functions and their `ref()` / `unref()`. # Run on mobile & native import { Cards, Card } from 'fumadocs-ui/components/card' Recipes for running Bare peer-to-peer logic inside mobile and native shells. # Type a native RPC bridge import { Steps, Step } from 'fumadocs-ui/components/steps' Once a [worklet](/how-to/run-on-native/embed-bare-in-react-native) is exchanging bytes with its host, the next problem is structure: framing messages and parsing them by hand on both sides is error-prone, and the two sides drift apart over time. This guide puts a [typed RPC seam](/explanation/bare-on-native#the-typed-rpc-seam) on the channel—methods generated from a single schema—as described in [One core, many platforms](/explanation/bare-on-native). The pieces: [`hyperschema`](https://github.com/holepunchto/hyperschema) defines the data structures and generates [`compact-encoding`](/reference/helpers/compact-encoding) codecs; [`bare-rpc`](/reference/modules/bare-modules) frames requests and replies over the IPC stream. Install the tools [#install-the-tools] ```sh skip="install-instruction" npm i hyperschema bare-rpc compact-encoding ``` Define a schema [#define-a-schema] Register your structures on a namespace. Because schemas are versioned and append-only, you can add optional fields later without breaking older peers: ```js skip="snippet-illustration" // build-schema.js const Hyperschema = require('hyperschema') const schema = Hyperschema.from('./schema') const ns = schema.namespace('app') ns.register({ name: 'message', fields: [ { name: 'id', type: 'uint', required: true }, { name: 'text', type: 'string' } ] }) Hyperschema.toDisk(schema) ``` Running this writes a `schema.json` (for versioning) and a generated `index.js` of `compact-encoding` definitions you resolve by name and version: ```js skip="snippet-illustration" const c = require('compact-encoding') const { resolveStruct } = require('./schema') const message = resolveStruct('@app/message', 1) const bytes = c.encode(message, { id: 1, text: 'hello' }) ``` To generate wire-compatible Swift types for an iOS shell, run the Swift toolchain (`hyperschema-swift`, `compact-encoding-swift`) against the same schema; for the C and Kotlin paths, see [One core, many platforms](/explanation/bare-on-native). Frame calls with bare-rpc [#frame-calls-with-bare-rpc] Give each method a unique command number. In the worklet (the core), construct an `RPC` over the `BareKit.IPC` stream and handle incoming requests: ```js skip="snippet-illustration" // inside the worklet import RPC from 'bare-rpc' import c from 'compact-encoding' import { resolveStruct } from './schema' const SEND_MESSAGE = 1 const message = resolveStruct('@app/message', 1) const { IPC } = BareKit const rpc = new RPC(IPC, (req) => { if (req.command === SEND_MESSAGE) { const { text } = c.decode(message, req.data) console.log('received:', text) req.reply('ok') } }) ``` On the host side, construct an `RPC` over the worklet's IPC and send a request: ```js skip="snippet-illustration" // in the React Native host import RPC from 'bare-rpc' import c from 'compact-encoding' const rpc = new RPC(worklet.IPC) const req = rpc.request(SEND_MESSAGE) req.send(c.encode(message, { id: 1, text: 'hello' })) const reply = await req.reply() console.log(reply.toString()) // ok ``` A Swift shell does the same through the generated `HRPC` class and a small transport delegate that forwards bytes to and from the worklet—no hand-written parsing. Stream when one message isn't enough [#stream-when-one-message-isnt-enough] For more than request/response, `bare-rpc` exposes streams on a request—`req.createRequestStream()` and `req.createResponseStream()`—covering the five patterns the seam supports: unary, send-only events, response-stream, request-stream, and duplex. ```js skip="snippet-illustration" const req = rpc.request(SUBSCRIBE) const updates = req.createResponseStream() for await (const chunk of updates) { // handle each streamed update } ``` See also [#see-also] * [One core, many platforms](/explanation/bare-on-native)—the architecture and the Swift/Android codegen story. * [`bare-kit` reference](/reference/bare/bare-kit)—the IPC channel this RPC rides on. * [Compact encoding](/reference/helpers/compact-encoding)—the codec `hyperschema` generates. * [Embed Bare in a React Native app](/how-to/run-on-native/embed-bare-in-react-native)—the worklet this seam connects to. # Back up photos in a peer-to-peer app import { Steps, Step } from 'fumadocs-ui/components/steps' This guide shows you how to **back up photos peer-to-peer** by adapting the [`hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron) scaffold to decode local images with [`bare-ffmpeg`](https://www.npmjs.com/package/bare-ffmpeg)/[`bare-media`](https://www.npmjs.com/package/bare-media) and push them into a [Hyperblobs](https://www.npmjs.com/package/hyperblobs) store. The reference implementation is [`pear-photo-backup`](https://github.com/holepunchto/pear-docs/tree/preview/examples/how-to/stream-and-share-media/photo-backup). {/* Shared snippet (content/_snippets/) included via Fumadocs by how-tos that have a frontend (UI), so they build on the desktop boilerplate. Carries a short portability nod too, so it REPLACES _pear-end-portability-callout on these guides—one orientation callout, not two. For how-tos whose logic works with either template, use _from-a-template-callout.mdx instead. See: https://www.fumadocs.dev/docs/markdown#include */} **Start from the desktop boilerplate.** This guide has a frontend, so it builds on [`hello-pear-electron`](/getting-started/from-a-template/start-from-hello-pear-electron)—the renderer, preload bridge, and Bare [worker](/explanation/workers) it extends. Read [Start from the hello-pear-electron template](/getting-started/from-a-template/start-from-hello-pear-electron) first. The Pear-end logic itself is portable—only the UI is desktop-specific (see [Runtime and languages](/explanation/runtime-and-languages)); the terminal template [`hello-pear-bare`](/getting-started/from-a-template/start-from-hello-pear-bare) has no UI layer. See all starting points in [Start from a template](/getting-started/from-a-template). * [Stream a live camera in a peer-to-peer app](/how-to/stream-and-share-media/stream-a-live-camera-in-a-peer-to-peer-app)—the sibling how-to that establishes the same Hyperblobs + blob-server pattern for live frames. Before you begin [#before-you-begin] * A working clone of `hello-pear-electron` (or your own app built from the getting-started path). * Comfort with [Hyperblobs](https://www.npmjs.com/package/hyperblobs) and the [Bare](/explanation/runtime-and-languages) native module set. What changes [#what-changes] | Layer | Change | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Dependencies | Add [`bare-ffmpeg`](https://www.npmjs.com/package/bare-ffmpeg), [`bare-media`](https://www.npmjs.com/package/bare-media), [`get-mime-type`](https://www.npmjs.com/package/get-mime-type), [`hyperblobs`](https://www.npmjs.com/package/hyperblobs), [`hypercore-blob-server`](https://www.npmjs.com/package/hypercore-blob-server), [`hypercore-id-encoding`](https://www.npmjs.com/package/hypercore-id-encoding). | | Worker | Store the full file as one Hyperblob and generate a small inline **preview** (a `data:` URL)—`bare-media` for images, `bare-ffmpeg` for video—recorded alongside the blob id in the view. | | Worker transport | Use a JSON-over-pipe control surface: the renderer sends `{ type: 'add-video', path }` and the worker emits `{ type: 'videos', videos }` events whose entries carry a blob-server link plus the inline preview. | | Renderer | Show a grid that renders each entry's inline preview and opens the full blob via its link. | Steps [#steps] Add the dependencies [#add-the-dependencies] ```bash skip="desktop-gui" npm install bare-ffmpeg bare-media get-mime-type hyperblobs hypercore-blob-server hypercore-id-encoding ``` `bare-ffmpeg` and `bare-media` are **Bare** native modules—they ship prebuilt binaries via [`bare-sidecar`](https://www.npmjs.com/package/bare-sidecar) and only work inside the Bare worker, never in Electron's main or renderer. Store the file and generate a preview inside the worker [#store-the-file-and-generate-a-preview-inside-the-worker] `workers/video-room.js` (`VideoRoom`, shared with the video-stream how-to but extended for images) defines `addVideo` (L181), which checks the MIME type with [`get-mime-type`](https://www.npmjs.com/package/get-mime-type) and rejects anything that is not an image or video (L183–L186). It then streams the full file bytes into a Hyperblobs write stream (L188–L194) and captures the resulting blob id (L195). Only then does it generate a small inline preview—`bare-media` for images, `bare-ffmpeg` for video (L197)—and append the record to the base (L200–L202): ```js file=/examples/how-to/stream-and-share-media/photo-backup/workers/video-room.js#L181-L203 title="workers/video-room.js" lineNumbers {181,183-186,188-194,195,197,200-202} skip="example-import" async addVideo (filePath, info) { const name = path.basename(filePath) const type = getMimeType(name) if (!(type.startsWith('image/') || type.startsWith('video/'))) { throw new Error('Only image/video files are allowed') } const rs = fs.createReadStream(filePath) const ws = this.blobs.createWriteStream() await new Promise((resolve, reject) => { ws.on('error', reject) ws.on('close', resolve) rs.pipe(ws) }) const blob = { key: idEnc.normalize(this.blobs.core.key), ...ws.id } const preview = type.startsWith('image/') ? await createPreviewImage(filePath) : await createPreviewVideo(filePath) const id = Math.random().toString(16).slice(2) await this.base.append( VideoDispatch.encode('@pear-photo-backup/add-video', { id, name, type, blob, info: { ...info, preview } }) ) } ``` The preview is a base64 `data:` URL kept inline in the record's `info`, so the grid can paint immediately without fetching the full blob. Images are resized with `bare-media`: `createPreviewImage` decodes the file, resizes it to a 256×256 bound, and re-encodes it as WebP (L6–L9), then returns the bytes as a base64 `data:` URL (L10): ```js file=/examples/how-to/stream-and-share-media/photo-backup/workers/create-preview-image.js title="workers/create-preview-image.js" lineNumbers {6-9,10} skip="example-import" const { image } = require('bare-media') const MIMETYPE = 'image/webp' async function createPreviewImage (filePath) { const buffer = await image(filePath) .decode() .resize({ maxWidth: 256, maxHeight: 256 }) .encode({ mimetype: MIMETYPE }) return `data:${MIMETYPE};base64,${buffer.toString('base64')}` } module.exports = createPreviewImage ``` `workers/create-preview-video.js` is the `bare-ffmpeg` counterpart for video files (a stub in the reference app—wire up `bare-ffmpeg` frame extraction here to generate a video thumbnail). Both preview helpers are worker-side modules (they call `bare-media`/`bare-ffmpeg`), so they live alongside the worker in `workers/`, not in the renderer. Each record stored in the view is `{ id, name, type, blob, info: { preview, ... } }`—small metadata plus a blob id pointing at the full bytes. Reuse the standard close order [#reuse-the-standard-close-order] Photo backup does not add an interval or any other timer, so `WorkerTask._close` keeps the same chain as hello-pear-electron: room → swarm → store. The room itself closes its blob server and blobs core first (`VideoRoom._close`). The [`graceful-goodbye`](https://www.npmjs.com/package/graceful-goodbye) handler in `workers/index.js` fires it. Render a photo grid [#render-a-photo-grid] In the renderer, render `` straight from the inline preview, and open the full-size image (or video) via `entry.info.link`—the blob-server URL the worker attaches in `getVideos`. Use the renderer's drag-and-drop to call `getPathForFile(file)` on each dropped file (exposed by the preload bridge) and write `{ type: 'add-video', path }` over the worker pipe, which the worker forwards to its `addVideo` handler. Run it [#run-it] ```bash skip="desktop-gui" npm run build # host npm start -- --storage /tmp/photos-host --name host ``` Drag and drop photos into the window. The thumbnails appear in the grid immediately. Quit the host, restart it with the same `--storage` path, and the grid replays from disk. ```bash skip="desktop-gui" # friend backing up the same album npm start -- --storage /tmp/photos-friend --name friend --invite ``` The friend's app replicates the room's Hyperblobs and shows the same grid. Originals are downloaded lazily—only the thumbnails are pulled eagerly. Where to go next [#where-to-go-next] * [Store and serve large media with Hyperblobs](/how-to/stream-and-share-media/store-and-serve-large-media-with-hyperblobs)—the Pear/Bare blob primitive behind this app, with no UI. * [Stream a live camera in a peer-to-peer app](/how-to/stream-and-share-media/stream-a-live-camera-in-a-peer-to-peer-app)—the same Hyperblobs + blob-server pattern for live frames. * [Stream stored video in a peer-to-peer app](/how-to/stream-and-share-media/stream-stored-video-in-a-peer-to-peer-app)—the same blob mechanics for stored video. * [Bare modules](/reference/modules/bare-modules)—what `bare-ffmpeg` and `bare-media` ship and why they live in the worker. # Create a full peer-to-peer filesystem with Hyperdrive This guide will show you how to create a full peer-to-peer filesystem with Hyperdrive. [Hyperdrive](/reference/building-blocks/hyperdrive) is a wrapper around two [Hypercores](/reference/building-blocks/hypercore): * one is a [Hyperbee](/reference/building-blocks/hyperbee) index for storing file metadata, and * the other is used to store file contents. In this guide, you will create three Pear Terminal Applications: * [`drive-writer-app`](#create-the-drive-writer-app) that mirrors a local directory into a Hyperdrive, * [`drive-reader-app`](#create-the-drive-reader-app) that replicates the Hyperdrive with a reader peer, and * [`drive-bee-reader-app`](#inspect-the-hyperdrive-as-a-hyperbee) that inspects the Hyperdrive as a Hyperbee When the writer modifies its drive—adding, removing, or changing files—the reader's local copy updates to match. To do this, you need two additional tools: * [`MirrorDrive`](/reference/helpers/mirrordrive): the diff-and-sync engine that mirrors one drive into another (used here via `local.mirror(drive)` and [`drive.mirror(local)`](/reference/building-blocks/hyperdrive#drivemirrorout-options)), and * [`LocalDrive`](/reference/helpers/localdrive): a local filesystem adapter that exposes a Hyperdrive-like interface over a directory on disk. These tools handle all interactions between Hyperdrives and the local filesystem. {/* Shared snippet (content/_snippets/) included via Fumadocs by the chat-app delta how-tos. Reminds readers these guides teach portable Pear-end (worker) logic, not desktop-specific code. See: https://www.fumadocs.dev/docs/markdown#include */} **This guide is about the Pear-end, not the shell.** The code below lives in the Bare [worker](/explanation/workers)—the peer-to-peer logic, not the user interface. Because the Pear-end never imports DOM APIs and never assumes a UI framework, the same worker is portable across **desktop (Electron)**, **mobile (React Native via Bare iOS / Bare Android)**, and **terminal**. The example apps ship an Electron shell, but only the UI half changes per platform—the logic here stays the same. See [Runtime and languages](/explanation/runtime-and-languages) for the cross-platform model and current support. **This guide focuses on the Pear/Bare logic**—plain terminal apps, no UI. For the same file-sharing capability wired into a full desktop app with a front end, see [Share files in a peer-to-peer app](/how-to/stream-and-share-media/share-files-in-a-peer-to-peer-app) and its worked example [`pear-file-sharing`](https://github.com/holepunchto/pear-docs/tree/preview/examples/how-to/stream-and-share-media/file-sharing). The drive logic is identical; only the UI half changes. Create the drive-writer-app [#create-the-drive-writer-app] Create the drive-writer-app directory and add dependencies [#create-the-drive-writer-app-directory-and-add-dependencies] Start by creating the `drive-writer-app` project with these commands: ```sh example=hyperdrive-fs step=setup mkdir drive-writer-app cd drive-writer-app npm init -y npm pkg set type="module" npm install corestore localdrive hyperswarm hyperdrive debounceify b4a bare-process ``` This will install the following dependencies: * [`bare-process`](https://www.npmjs.com/package/bare-process): A module for working with processes. * [`hyperswarm`](/reference/building-blocks/hyperswarm): A module for working with Hyperswarm. * [`hyperdrive`](/reference/building-blocks/hyperdrive): A module for working with Hyperdrive. * [`localdrive`](/reference/helpers/localdrive): A module for working with Localdrive. * [`corestore`](/reference/helpers/corestore): A module for working with Corestore. * [`debounceify`](https://www.npmjs.com/package/debounceify): A module for debouncing functions. * [`b4a`](https://www.npmjs.com/package/b4a): A module for working with buffers. Add the drive-writer-app logic [#add-the-drive-writer-app-logic] Create the `drive-writer-app/index.js` file with the following content: ```javascript file=/examples/how-to/stream-and-share-media/create-a-full-peer-to-peer-filesystem-with-hyperdrive/drive-writer-app/index.js title="drive-writer-app/index.js" lineNumbers {11-16,19-23,26,31-34,39-42,45-49} skip="example-import" import process from 'bare-process' import Hyperswarm from 'hyperswarm' import Hyperdrive from 'hyperdrive' import Localdrive from 'localdrive' import Corestore from 'corestore' import debounce from 'debounceify' import b4a from 'b4a' // create a Corestore instance const store = new Corestore('./drive-writer-storage') const swarm = new Hyperswarm() process.once('SIGINT', () => swarm.destroy().then(() => process.exit(0))) // replication of the corestore instance on connection with other peers swarm.on('connection', conn => store.replicate(conn)) // A local drive provides a Hyperdrive interface to a local directory const local = new Localdrive('./writer-dir') // A Hyperdrive takes a Corestore because it needs to create many cores // One for a file metadata Hyperbee, and one for a content Hypercore const drive = new Hyperdrive(store) // wait till the properties of the hyperdrive instance are initialized await drive.ready() // Import changes from the local drive into the Hyperdrive const mirror = debounce(mirrorDrive) const discovery = swarm.join(drive.discoveryKey) await discovery.flushed() console.log('drive key:', b4a.toString(drive.key, 'hex')) // start the mirroring process (i.e copying) of content from writer-dir to the drive // whenever something is entered (other than '/n' or Enter )in the command-line process.stdin.setEncoding('utf-8') process.stdin.on('data', (data) => { if (!data.match('\n')) return mirror() }) // this function copies the contents from writer-dir directory to the drive async function mirrorDrive () { console.log('started mirroring changes from \'./writer-dir\' into the drive...') const mirror = local.mirror(drive) await mirror.done() console.log('finished mirroring:', mirror.count) } ``` The `drive-writer-app` creates a [`Corestore`](/reference/helpers/corestore) and replicates it over [`Hyperswarm`](/reference/building-blocks/hyperswarm) so other peers can fetch the data (L11–L16). It wraps the local `./writer-dir` directory in a [`Localdrive`](/reference/helpers/localdrive) and creates the [`Hyperdrive`](/reference/building-blocks/hyperdrive) on top of the store (L19–L23), then waits for it to initialize (L26). After joining the swarm on the drive's discovery key, it prints the drive key that [`drive-reader-app`](#create-the-drive-reader-app) will need (L31–L34). Pressing `Enter` in the terminal triggers a debounced mirror (L39–L42), and `mirrorDrive` copies the contents of `./writer-dir` into the drive (L45–L49). {/* @harness example=hyperdrive-fs step=copy from=examples/how-to/stream-and-share-media/create-a-full-peer-to-peer-filesystem-with-hyperdrive/drive-writer-app/index.js to=drive-writer-app/index.js */} Run the `drive-writer-app` with: ```sh example=hyperdrive-fs step=run process=writer expect="drive key:" capture-key="drive key: ([0-9a-f]+)" timeout=45000 bare drive-writer-app ``` It outputs a key which will be passed to [`drive-reader-app`](#create-the-drive-reader-app) upon execution. Create the drive-reader-app [#create-the-drive-reader-app] Create the drive-reader-app directory and add dependencies [#create-the-drive-reader-app-directory-and-add-dependencies] Leave the [`drive-writer-app`](#create-the-drive-writer-app) running and in a new terminal create the `drive-reader-app` project with the following commands: ```sh example=hyperdrive-fs step=setup mkdir drive-reader-app cd drive-reader-app npm init -y npm pkg set type="module" npm install corestore localdrive hyperswarm hyperdrive debounceify b4a bare-process ``` This will install the following dependencies: * [`bare-process`](https://www.npmjs.com/package/bare-process): A module for working with processes. * [`hyperswarm`](/reference/building-blocks/hyperswarm): A module for working with Hyperswarm. * [`hyperdrive`](/reference/building-blocks/hyperdrive): A module for working with Hyperdrive. * [`localdrive`](/reference/helpers/localdrive): A module for working with Localdrive. * [`corestore`](/reference/helpers/corestore): A module for working with Corestore. * [`debounceify`](https://www.npmjs.com/package/debounceify): A module for debouncing functions. * [`b4a`](https://www.npmjs.com/package/b4a): A module for working with buffers. Add the drive-reader-app logic [#add-the-drive-reader-app-logic] Create the `drive-reader-app/index.js` file with the following content: ```javascript file=/examples/how-to/stream-and-share-media/create-a-full-peer-to-peer-filesystem-with-hyperdrive/drive-reader-app/index.js title="drive-reader-app/index.js" lineNumbers {10-12,15-21,24-27,32-36,39-42,44-48} skip="example-import" import process from 'bare-process' import Hyperswarm from 'hyperswarm' import Hyperdrive from 'hyperdrive' import Localdrive from 'localdrive' import Corestore from 'corestore' import debounce from 'debounceify' import b4a from 'b4a' const key = Bare.argv[2] if (!key) throw new Error('provide a key') // create a Corestore instance const store = new Corestore('./drive-reader-storage') const swarm = new Hyperswarm() process.once('SIGINT', () => swarm.destroy().then(() => process.exit(0))) // replication of store on connection with other peers swarm.on('connection', conn => store.replicate(conn)) // create a local copy of the remote drive const local = new Localdrive('./reader-dir') // create a hyperdrive using the public key passed as a command-line argument const drive = new Hyperdrive(store, b4a.from(key, 'hex')) // wait till all the properties of the drive are initialized await drive.ready() const mirror = debounce(mirrorDrive) // call the mirror function whenever content gets appended // to the Hypercore instance of the hyperdrive drive.core.on('append', mirror) // join a topic swarm.join(drive.discoveryKey, { client: true, server: false }) // start the mirroring process (i.e copying the contents from remote drive to local dir) mirror() async function mirrorDrive () { console.log('started mirroring remote drive into \'./reader-dir\'...') const mirror = drive.mirror(local) await mirror.done() console.log('finished mirroring:', mirror.count) } ``` The `drive-reader-app` reads the writer's drive key from the command-line arguments (L10–L12) and replicates a [`Corestore`](/reference/helpers/corestore) over [`Hyperswarm`](/reference/building-blocks/hyperswarm) (L15–L21). It wraps the local `./reader-dir` directory in a [`Localdrive`](/reference/helpers/localdrive) and opens the remote [`Hyperdrive`](/reference/building-blocks/hyperdrive) from that key (L24–L27). It re-runs a debounced mirror whenever new content is appended to the drive's core (L32–L36), joins the swarm as a client and runs the first mirror immediately (L39–L42). `mirrorDrive` copies the contents of the remote drive into `./reader-dir` and logs when each pass completes (L44–L48). {/* @harness example=hyperdrive-fs step=copy from=examples/how-to/stream-and-share-media/create-a-full-peer-to-peer-filesystem-with-hyperdrive/drive-reader-app/index.js to=drive-reader-app/index.js */} Run the drive-reader-app [#run-the-drive-reader-app] Run the `drive-reader-app` with `bare`, passing the key printed by the [`drive-writer-app`](#create-the-drive-writer-app): ```sh example=hyperdrive-fs step=run process=reader cmd="bare drive-reader-app ${key}" expect="started mirroring remote drive" timeout=45000 bare drive-reader-app ``` `LocalDrive` does not create the directory passed to it until something has been written, so create `writer-dir` (`mkdir writer-dir`) beside the app directories before importing files. The next section walks through a full round trip from `writer-dir` to `reader-dir`. Just as a [Hyperbee](/reference/building-blocks/hyperbee) is **just** a [Hypercore](/reference/building-blocks/hypercore), a Hyperdrive is **just** a Hyperbee - which is **just** a Hypercore. Test the filesystem [#test-the-filesystem] With both apps still running, add, remove, or modify files inside `writer-dir`, then press `Enter` in the writer's terminal to import those local changes into the drive. Observe that the new changes mirror into `reader-dir`. For example, create one file in the writer directory: ```sh example=hyperdrive-fs step=setup mkdir -p writer-dir printf 'hello from hyperdrive\n' > writer-dir/hello.txt ``` {/* @harness example=hyperdrive-fs step=send process=writer data="\n" delay=500 */} {/* @harness example=hyperdrive-fs step=expect process=writer contains="started mirroring changes from './writer-dir' into the drive..." timeout=45000 */} Inspect the Hyperdrive as a Hyperbee [#inspect-the-hyperdrive-as-a-hyperbee] `Hyperdrive` exposes its metadata index as [`drive.db`](/reference/building-blocks/hyperdrive#drivedb), which is the underlying Hyperbee backing the file structure. You can inspect that Hyperbee directly when you want to see the raw file-entry metadata that [`drive.entry()`](/reference/building-blocks/hyperdrive#await-driveentrypath-options) wraps, or when you want to access the metadata index directly. Create the drive-bee-reader-app directory and add dependencies [#create-the-drive-bee-reader-app-directory-and-add-dependencies] In a new terminal, create the `drive-bee-reader-app` project with these commands: ```sh example=hyperdrive-fs step=setup mkdir drive-bee-reader-app cd drive-bee-reader-app npm init -y npm pkg set type="module" npm install corestore hyperswarm hyperdrive b4a bare-process ``` This will install the following dependencies: * [`bare-process`](https://www.npmjs.com/package/bare-process): A module for working with processes. * [`hyperswarm`](/reference/building-blocks/hyperswarm): A module for working with Hyperswarm. * [`corestore`](/reference/helpers/corestore): A module for working with Corestore. * [`hyperdrive`](/reference/building-blocks/hyperdrive): A module for working with Hyperdrive. * [`b4a`](https://www.npmjs.com/package/b4a): A module for working with buffers. Add the drive-bee-reader-app logic [#add-the-drive-bee-reader-app-logic] Create the `drive-bee-reader-app/index.js` file with the following content: ```javascript file=/examples/how-to/stream-and-share-media/create-a-full-peer-to-peer-filesystem-with-hyperdrive/drive-bee-reader-app/index.js title="drive-bee-reader-app/index.js" lineNumbers {8-10,12-17,19-21,24,29-37,39,41-49} skip="example-import" import process from 'bare-process' import Hyperswarm from 'hyperswarm' import Hyperdrive from 'hyperdrive' import Corestore from 'corestore' import b4a from 'b4a' const key = Bare.argv[2] if (!key) throw new Error('provide a key') const store = new Corestore('./drive-bee-reader-storage') const swarm = new Hyperswarm() process.once('SIGINT', () => swarm.destroy().then(() => process.exit(0))) swarm.on('connection', conn => store.replicate(conn)) const drive = new Hyperdrive(store, b4a.from(key, 'hex')) await drive.ready() await drive.db.ready() // Hyperdrive stores file metadata in the "files" sub-bee. const files = drive.db.sub('files', { keyEncoding: 'utf-8' }) const discovery = swarm.join(drive.discoveryKey, { client: true, server: false }) await discovery.flushed() let dbEntry = null for (let attempt = 0; attempt < 60; attempt++) { await drive.update() dbEntry = await files.peek() if (dbEntry) break await new Promise(resolve => setTimeout(resolve, 500)) } if (!dbEntry) throw new Error('expected at least one file entry to appear in drive.db') const driveEntry = await drive.entry(dbEntry.key) console.log('hyperbee entry:', JSON.stringify({ key: dbEntry.key, value: dbEntry.value })) console.log('drive entry:', JSON.stringify({ key: driveEntry.key, value: driveEntry.value })) await swarm.destroy() await drive.close() ``` Like the reader, this app reads the drive key from the command line (L8–L10) and replicates a [`Corestore`](/reference/helpers/corestore) over [`Hyperswarm`](/reference/building-blocks/hyperswarm) (L12–L17). It opens the [`Hyperdrive`](/reference/building-blocks/hyperdrive) and awaits both [`drive.ready()`](/reference/building-blocks/hyperdrive#await-driveready) and `drive.db.ready()` (L19–L21), then reaches into the metadata index directly: file entries live in the `files` sub-[Hyperbee](/reference/building-blocks/hyperbee) (L24). It polls [`drive.update()`](/reference/building-blocks/hyperdrive#await-driveupdateoptions) until the first entry replicates in (L29–L37), looks the same file up through the higher-level `drive.entry()` API (L39), and logs both the raw Hyperbee entry and the `drive.entry()` view side by side (L41–L49). {/* @harness example=hyperdrive-fs step=copy from=examples/how-to/stream-and-share-media/create-a-full-peer-to-peer-filesystem-with-hyperdrive/drive-bee-reader-app/index.js to=drive-bee-reader-app/index.js */} Run the drive-bee-reader-app [#run-the-drive-bee-reader-app] Run the `drive-bee-reader-app` with `bare`, passing the key printed by the [`drive-writer-app`](#create-the-drive-writer-app): ```sh example=hyperdrive-fs step=run process=bee-reader cmd="bare drive-bee-reader-app ${key}" expect="hyperbee entry:" timeout=45000 bare drive-bee-reader-app ``` See also [#see-also] * [Share files in a peer-to-peer app](/how-to/stream-and-share-media/share-files-in-a-peer-to-peer-app)—the same Hyperdrive sharing wrapped in a desktop UI. * [Storage and distribution](/explanation/storage-and-distribution)—where these drives live on disk and how peers receive new versions. * [Deploy your application](/how-to/operate-an-app/manual-deployment/deployment)—staging and seeding when you're ready to ship. * [Hyperdrive reference](/reference/building-blocks/hyperdrive)—full API for the distributed filesystem used in this guide. * [Corestore reference](/reference/helpers/corestore)—full API for the shared storage and replication manager passed to Hyperdrive. * [Localdrive reference](/reference/helpers/localdrive)—full API for the local filesystem adapter mirrored to and from Hyperdrive. * [Mirrordrive reference](/reference/helpers/mirrordrive)—full API for the diff-and-sync engine driving `drive.mirror(...)` and `local.mirror(...)` here. # Stream and share media import { Cards, Card } from 'fumadocs-ui/components/card' Recipes for moving files and streaming media directly between peers. # Share files in a peer-to-peer app import { Steps, Step } from 'fumadocs-ui/components/steps' This guide shows you how to **swap the [`hello-pear-electron`](https://github.com/holepunchto/hello-pear-electron) room for a [Hyperdrive](/reference/building-blocks/hyperdrive)** so peers share files instead of messages. The reference implementation is [`pear-file-sharing`](https://github.com/holepunchto/pear-docs/tree/preview/examples/how-to/stream-and-share-media/file-sharing). {/* Shared snippet (content/_snippets/) included via Fumadocs by how-tos that have a frontend (UI), so they build on the desktop boilerplate. Carries a short portability nod too, so it REPLACES _pear-end-portability-callout on these guides—one orientation callout, not two. For how-tos whose logic works with either template, use _from-a-template-callout.mdx instead. See: https://www.fumadocs.dev/docs/markdown#include */} **Start from the desktop boilerplate.** This guide has a frontend, so it builds on [`hello-pear-electron`](/getting-started/from-a-template/start-from-hello-pear-electron)—the renderer, preload bridge, and Bare [worker](/explanation/workers) it extends. Read [Start from the hello-pear-electron template](/getting-started/from-a-template/start-from-hello-pear-electron) first. The Pear-end logic itself is portable—only the UI is desktop-specific (see [Runtime and languages](/explanation/runtime-and-languages)); the terminal template [`hello-pear-bare`](/getting-started/from-a-template/start-from-hello-pear-bare) has no UI layer. See all starting points in [Start from a template](/getting-started/from-a-template). * [Create a full peer-to-peer filesystem with Hyperdrive](/how-to/stream-and-share-media/create-a-full-peer-to-peer-filesystem-with-hyperdrive)—the building block this guide layers a desktop UI on top of. Before you begin [#before-you-begin] * A working clone of `hello-pear-electron` (or your own app built from the getting-started path). * Familiarity with [Hyperdrive](/reference/building-blocks/hyperdrive) and [Localdrive](/reference/helpers/localdrive). What changes [#what-changes] | Layer | Change | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Dependencies | Add [`hyperdrive`](https://www.npmjs.com/package/hyperdrive), [`localdrive`](https://www.npmjs.com/package/localdrive), and [`hypercore-id-encoding`](https://www.npmjs.com/package/hypercore-id-encoding). | | Worker | Add a `DriveRoom`: each peer owns a [Hyperdrive](/reference/building-blocks/hyperdrive) mirrored from a local `my-drive` folder, and the [Autobase](/reference/building-blocks/autobase) view tracks the set of drive keys so peers mirror each others' drives into `shared-drives`. | | Worker teardown | Cancel the mirror/file-list interval timers in `_close` before closing the room → swarm → store. | | Worker messages | Push a `drives` message (each drive plus its files) and handle an `add-file` message that copies a chosen file into `my-drive`. | | Renderer | Render a per-drive file list with an "add file" picker. | Steps [#steps] Add the dependencies [#add-the-dependencies] ```bash skip="desktop-gui" npm install hyperdrive localdrive hypercore-id-encoding ``` Add a DriveRoom worker [#add-a-driveroom-worker] Create `workers/drive-room.js` (`DriveRoom`) by adapting `chat-room.js`. The pairing, [Autobase](/reference/building-blocks/autobase), and writer plumbing stay the same; what changes is the data each peer publishes: * Each peer owns one **[Hyperdrive](/reference/building-blocks/hyperdrive)** (`this.myDrive`) backed by a local `my-drive` folder (`this.myLocalDrive`, a [`localdrive`](https://www.npmjs.com/package/localdrive)). `_uploadMyDrive` (L160) publishes the drive key to the Autobase (L162), joins the swarm on it (L163), and mirrors the folder into the Hyperdrive on a 1-second timer (L165–L166), so dropping a file into `my-drive` publishes it to peers. * The Autobase view stores just the **set of drive keys** (`@pear-file-sharing/drives`). `_downloadSharedDrives` (L141) replicates every peer's Hyperdrive: it opens a per-key `LocalDrive` under `shared-drives` (L147), reuses `myDrive` or constructs a peer Hyperdrive from the key (L149), mirrors the drive down on every `append` (L152–L153), and joins the swarm on its discovery key (L156). ```js file=/examples/how-to/stream-and-share-media/file-sharing/workers/drive-room.js#L141-L167 title="workers/drive-room.js" lineNumbers {141,147,149,152-153,156,160,162-163,165-166} skip="example-import" async _downloadSharedDrives () { const drives = await this.getDrives() await Promise.all(drives.map(async (item) => { const key = idEnc.normalize(item.key) if (this.drives[key]) return const local = new LocalDrive(path.join(this.sharedDrivesPath, key)) this.localDrives[key] = local const drive = key === idEnc.normalize(this.myDrive.key) ? this.myDrive : new Hyperdrive(this.store, item.key) this.drives[key] = drive const mirror = debounce(() => drive.mirror(local).done()) drive.core.on('append', () => mirror()) await drive.ready() this.swarm.join(drive.discoveryKey) })) } async _uploadMyDrive () { await this.myDrive.ready() this.addDrive(this.myDrive.key, { name: this.name }) this.swarm.join(this.myDrive.discoveryKey) const mirror = debounce(() => this.myLocalDrive.mirror(this.myDrive).done()) this.uploadInterval = setInterval(() => mirror(), 1000) } ``` Preserve the clearInterval teardown [#preserve-the-clearinterval-teardown] `pear-file-sharing` runs two polling loops: `DriveRoom._uploadMyDrive` mirrors the user's `my-drive` folder into the Hyperdrive, and `WorkerTask` rebuilds the file list for the renderer. Both store their timer handles, and `_close` clears them (L62) before the standard room → swarm → store chain (L63–L65). **Do not drop this**, or shutdown leaks an interval timer: ```js file=/examples/how-to/stream-and-share-media/file-sharing/workers/worker-task.js#L61-L66 title="workers/worker-task.js" lineNumbers {62,63-65} skip="example-import" async _close () { clearInterval(this.intervalFiles) await this.room.close() await this.swarm.destroy() await this.store.close() } ``` `DriveRoom._close` does the same for its own `uploadInterval`. The [`graceful-goodbye`](https://www.npmjs.com/package/graceful-goodbye) hook in `workers/index.js` is what fires this on SIGINT / IPC end. Surface the drives over the worker pipe [#surface-the-drives-over-the-worker-pipe] The worker uses a HyperDispatch for its Autobase, with `add-drive` standing in for `add-message` (`schema.js` registers the `drive`/`drives` schemas and the `add-drive` dispatch). Regenerate `spec/`: ```bash skip="desktop-gui" npm run build:db ``` The worker–renderer transport stays the plain-JSON-over-[`framed-stream`](https://www.npmjs.com/package/framed-stream) pipe in hello-pear-electron—no HRPC. `WorkerTask._open` wires both ends: it parses each plain-JSON message off the pipe (L44–L50), and an `add-file` message copies the chosen file into the `my-drive` folder (L51–L53) where `_uploadMyDrive` picks it up. A 1-second interval starts `_drives` (L56), and the initial invite is written back to the renderer (L58): ```js file=/examples/how-to/stream-and-share-media/file-sharing/workers/worker-task.js#L37-L59 title="workers/worker-task.js" lineNumbers {44-50,51-53,56,58} skip="example-import" async _open () { await this.store.ready() await this.room.ready() await fs.promises.mkdir(this.myDrivePath, { recursive: true }) await fs.promises.mkdir(this.sharedDrivesPath, { recursive: true }) this.pipe.on('data', async (data) => { let message try { message = JSON.parse(data) } catch { return } if (message.type === 'add-file') { await fs.promises.copyFile(message.uri, path.join(this.myDrivePath, message.name)) } }) this.intervalFiles = setInterval(() => this._drives(), 1000) this.pipe.write(JSON.stringify({ type: 'invite', invite: await this.room.getInvite() })) } ``` `_drives` reads each mirrored drive folder off disk and writes the full list—drive name plus its files as `file://` URIs—back to the renderer as a `drives` message. It walks every known drive (L69), reads its mirrored folder recursively (L74–L77), builds the drive plus its files as `file://` URIs (L84–L85), pins the user's own drive first (L89–L92), and writes the `drives` message over the pipe (L94): ```js file=/examples/how-to/stream-and-share-media/file-sharing/workers/worker-task.js#L68-L95 title="workers/worker-task.js" lineNumbers {69,74-77,84-85,89-92,94} skip="example-import" async _drives () { const rawDrives = await this.room.getDrives() const drives = await Promise.all(rawDrives.map(async (drive) => { const key = idEnc.normalize(drive.key) const dir = path.join(this.sharedDrivesPath, key) await fs.promises.mkdir(dir, { recursive: true }) const files = await fs.promises.readdir(dir, { recursive: true }).catch((err) => { if (err.code === 'ENOENT') return [] throw err }) const isMyDrive = key === idEnc.normalize(this.room.myDrive.key) return { ...drive, info: { ...drive.info, isMyDrive, uri: `file://${isMyDrive ? this.myDrivePath : dir}`, files: files.map((name) => ({ name, uri: `file://${path.join(dir, name)}` })) } } })) drives.sort((a, b) => { if (a.info.isMyDrive && !b.info.isMyDrive) return -1 if (!a.info.isMyDrive && b.info.isMyDrive) return 1 return a.info.name.localeCompare(b.info.name) }) this.pipe.write(JSON.stringify({ type: 'drives', drives })) } ``` Update the renderer [#update-the-renderer] In the vanilla `renderer/app.js`, render a list grouped by drive—each peer's drive and its files, with the user's own drive pinned first. Add a drag-and-drop zone plus a "browse" file picker that send an `add-file` message over the worker pipe. Use `bridge.getPathForFile(file)` (L35)—backed by `webUtils.getPathForFile`, already exposed in `electron/preload.js` of hello-pear-electron—to turn each picked file into a local path the worker can copy, then send it as an `add-file` message (L36). `renderDrives` builds one card per drive and links each file to its `file://` URI (L43–L100). The drop zone (L111–L115) and the "browse" picker (L117–L120) both feed `addFiles`, and incoming worker messages route `drives`/`invite` events to the renderer (L131–L132): ```js file=/examples/how-to/stream-and-share-media/file-sharing/renderer/app.js title="renderer/app.js" lineNumbers {35,36,43-100,111-115,117-120,131-132} skip="example-import" const bridge = window.bridge const decoder = new TextDecoder('utf-8') const SPECIFIER = '/workers/index.js' const countEl = document.getElementById('count') const dropzoneEl = document.getElementById('dropzone') const fileInputEl = document.getElementById('fileInput') const drivesEl = document.getElementById('drives') const emptyEl = document.getElementById('empty') const inviteBarEl = document.getElementById('invite-bar') const inviteEl = document.getElementById('invite') const copyEl = document.getElementById('copy') let invite = '' function setInvite (value) { invite = value if (!invite) { inviteBarEl.classList.add('hidden') return } inviteEl.textContent = invite inviteBarEl.classList.remove('hidden') } copyEl.addEventListener('click', () => { if (!invite) return bridge.writeClipboard(invite) copyEl.textContent = 'Copied' setTimeout(() => { copyEl.textContent = 'Copy' }, 1500) }) function addFile (file) { const uri = bridge.getPathForFile(file) bridge.writeWorkerIPC(SPECIFIER, JSON.stringify({ type: 'add-file', name: file.name, uri })) } function addFiles (files) { for (const file of files) addFile(file) } function renderDrives (drives) { const totalFiles = drives.reduce((sum, d) => sum + d.info.files.length, 0) countEl.textContent = `${drives.length} drive${drives.length === 1 ? '' : 's'} · ${totalFiles} file${totalFiles === 1 ? '' : 's'}` // Re-render the list from scratch; keep the empty-state element in the DOM. for (const node of [...drivesEl.children]) { if (node !== emptyEl) node.remove() } emptyEl.style.display = drives.length === 0 ? '' : 'none' for (const drive of drives) { const card = document.createElement('div') card.className = 'rounded-2xl border border-neutral-800 bg-neutral-900 px-4 py-3' const head = document.createElement('div') head.className = 'flex items-center gap-2 mb-2' const title = document.createElement('a') title.className = 'text-sm font-medium text-neutral-100 hover:text-white hover:underline truncate' title.href = drive.info.uri title.textContent = drive.info.name head.append(title) if (drive.info.isMyDrive) { const badge = document.createElement('span') badge.className = 'rounded-full bg-neutral-800 px-2 py-0.5 text-[10px] uppercase tracking-wider text-neutral-400' badge.textContent = 'You' head.append(badge) } card.append(head) if (drive.info.files.length === 0) { const empty = document.createElement('div') empty.className = 'text-xs text-neutral-500' empty.textContent = 'Empty drive.' card.append(empty) } else { const list = document.createElement('ul') list.className = 'space-y-1' for (const file of drive.info.files) { const item = document.createElement('li') item.className = 'text-sm' const link = document.createElement('a') link.className = 'text-neutral-300 hover:text-neutral-100 hover:underline break-all' link.href = file.uri link.textContent = file.name item.append(link) list.append(item) } card.append(list) } drivesEl.append(card) } } dropzoneEl.addEventListener('dragover', (event) => { event.preventDefault() dropzoneEl.classList.add('border-neutral-600', 'bg-neutral-900') }) dropzoneEl.addEventListener('dragleave', () => { dropzoneEl.classList.remove('border-neutral-600', 'bg-neutral-900') }) dropzoneEl.addEventListener('drop', (event) => { event.preventDefault() dropzoneEl.classList.remove('border-neutral-600', 'bg-neutral-900') addFiles(event.dataTransfer.files) }) fileInputEl.addEventListener('change', (event) => { addFiles(event.target.files) event.target.value = '' }) bridge.startWorker(SPECIFIER) const offWorkerIPC = bridge.onWorkerIPC(SPECIFIER, (data) => { let message try { message = JSON.parse(decoder.decode(data)) } catch { return } if (message.type === 'drives') renderDrives(message.drives) if (message.type === 'invite') setInvite(message.invite) }) const offWorkerExit = bridge.onWorkerExit(SPECIFIER, (code) => { console.log('worker exited with code', code) offWorkerIPC() offWorkerExit() }) ``` Run it [#run-it] ```bash skip="desktop-gui" npm run build # user1: create room + print invite + watch folder npm start -- --storage /tmp/files-user1 --name user1 ``` Drop files into the path printed as `My drive:` in the terminal. They appear in the file list. In a second terminal: ```bash skip="desktop-gui" npm start -- --storage /tmp/files-user2 --name user2 --invite ``` user2's app lists user1's files. They are mirrored down into the `shared-drives` folder automatically, and each entry links to the local `file://` path on disk. Where to go next [#where-to-go-next] * [Create a full peer-to-peer filesystem with Hyperdrive](/how-to/stream-and-share-media/create-a-full-peer-to-peer-filesystem-with-hyperdrive)—the underlying mechanics. * [Stream stored video in a peer-to-peer app](/how-to/stream-and-share-media/stream-stored-video-in-a-peer-to-peer-app)—same scaffold, range-served blobs instead of files. * [From append-only logs to files](/explanation/from-logs-to-files)—why a [Hyperdrive](/reference/building-blocks/hyperdrive) ends up looking like a filesystem. # Store and serve large media with Hyperblobs **This guide focuses on the Pear/Bare logic.** It shows [`hyperblobs`](https://www.npmjs.com/package/hyperblobs) and [`hypercore-blob-server`](https://www.npmjs.com/package/hypercore-blob-server) on their own—no Electron, no UI. For the same blob plumbing wired into full desktop apps, see [Stream stored video in a peer-to-peer app](/how-to/stream-and-share-media/stream-stored-video-in-a-peer-to-peer-app), [Stream a live camera in a peer-to-peer app](/how-to/stream-and-share-media/stream-a-live-camera-in-a-peer-to-peer-app), and [Back up photos in a peer-to-peer app](/how-to/stream-and-share-media/back-up-photos-in-a-peer-to-peer-app). The blob logic is identical across all three; only the UI and the frame/file source change. To share a whole folder of files over [Hyperdrive](/reference/building-blocks/hyperdrive) instead, see [Share files in a peer-to-peer app](/how-to/stream-and-share-media/share-files-in-a-peer-to-peer-app). A [Hypercore](/reference/building-blocks/hypercore) is an append-only log of small blocks—great for messages, awkward for a 200 MB video. [`Hyperblobs`](https://www.npmjs.com/package/hyperblobs) solves that: it chunks arbitrarily large binary data across a Hypercore and hands back a small **blob id** that addresses it. [`hypercore-blob-server`](https://www.npmjs.com/package/hypercore-blob-server) then serves any blob over local HTTP (`127.0.0.1`) so a `