Start from the hello-pear-bare template
An alternative getting started path for terminal apps: clone the hello-pear-bare boilerplate and learn how a standalone Bare process wires pear-runtime over-the-air updates, Corestore, and Hyperswarm—then build it into a cross-platform binary.
This is the terminal counterpart to Start from the hello-pear-electron template. 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 is Holepunch's official boilerplate for peer-to-peer terminal applications. Where the Electron template embeds pear-runtime into Electron, this template embeds it into Bare, 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 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.
New to peer-to-peer? Peer-to-peer, demystified explains how peers find each other and connect directly over the DHT, and The Pear stack maps how Bare, pear-runtime, Hyperswarm, and Corestore fit 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.
Need the pear CLI? Install it from install.pears.com, or prefix any command below with npx. See Install & upgrade for details.
Clone and run
1. Clone the repository
Clone the repository and install dependencies with the following commands:
git clone https://github.com/holepunchto/hello-pear-bare
cd hello-pear-bare
npm install2. 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:
pear touchThis prints a link, for example: pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o.
3. Set the upgrade link in package.json
Set the upgrade field in package.json to the link you just created:
"upgrade": "pear://qxenz5wmspmryjc13m9yzsqj1conqotn8fb4ocbufwtz9mtbqq5o"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.
npm startTo exercise the updater locally, opt back in:
npm start -- --updatesMap the template
| Path | What it is | Do you edit it? |
|---|---|---|
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 | The App class (a ready-resource). Spawns the Bare worker with PearRuntime.run, wraps its IPC in a FramedStream, and turns updater messages into events. | Sometimes—app lifecycle. |
workers/main.js | The Bare worker that owns the peer-to-peer code and the pear-runtime updater. | Yes—this is your backend. |
package.json | App metadata, scripts, the upgrade link, and the per-platform make:* build targets. | Yes—branding and release link. |
scripts/make.js | Detects the host OS and architecture and runs the matching make: target. | Rarely. |
test/index.js | The brittle test entry, run by npm test. | Yes—your tests. |
Like the Electron template, the peer-to-peer logic lives in a Bare worker; 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 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 cores, join Hyperswarm topics, and run your protocol.
How it's wired
The template is three layers: bin.mjs (entry + CLI), app.js (the host that runs the worker and updater), and the Bare worker that holds your peer-to-peer code.
bin.mjs is the Bare entry process. It parses three flags with paparam—--version, --storage, and --no-updates—then constructs the App:
const cmd = command(
appName,
summary(pkg.description),
flag('--version|-v', 'Print the current version'),
flag('--storage <dir>', '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, and --storage overrides both—see Storage and distribution:
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. When it opens, it spawns the Bare worker with PearRuntime.run—passing the runtime config as arguments—and wraps the worker's IPC pipe in a FramedStream:
_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 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:
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))Build a standalone binary
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 installed. Build for your current host with:
npm run makescripts/make.js detects your OS and architecture and runs the matching target, writing the binary to out/<platform>-<arch>. To build for a specific target—for example, in CI—call that target directly:
npm run make:darwin-arm64The 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
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 a release, pull it directly with the pear install command:
pear install pear://<key>After the first install, new releases reach users through the swarm—no reinstall needed. For how staging and seeding work, see Deploy your application and Installing applications.
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). Flags are parsed with paparam, so adding one is a two-line change. Add a --name flag to the command definition:
const cmd = command(
appName,
summary(pkg.description),
flag('--version|-v', 'Print the current version'),
flag('--storage <dir>', 'custom storage directory'),
- flag('--no-updates', 'disable OTA updates for this run')
+ flag('--no-updates', 'disable OTA updates for this run'),
+ flag('--name <name>', 'who to greet on startup')
)Then log the greeting after the other startup logs:
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 (workers/main.js):
- open a Corestore core,
- join a 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.
Customize for your brand
Before you ship, set your identity in package.json:
name,productName,description,author, andlicense.- the
upgradepear://link (see 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.
Where to go next
- Start from a template—both boilerplates, side by side.
- Start from the hello-pear-electron template—the desktop counterpart, with a renderer, preload bridge, and Bare worker.
- Inside Bare—what the runtime this template builds on actually is, and its lifecycle.
- The Pear stack—where Bare and Pear sit relative to each other.
- Runtime and languages—where Bare fits among Pear's runtimes.
- Bundle a Bare app—the
bare-packandbare-buildpaths behindnpm run make. - Bare modules—the Bare standard library this template builds on.
- Pear OTA—the
pear-runtimeAPI behind the updater. - Migrate from pear run to Pear OTA—if you're moving an existing app onto
pear-runtime. - Publish with GitHub Actions—stage a stable
pear://link on every push with thepear-ciaction. - Deploy your application—the full-control manual path for staging, provisioning, and release lines.
- Seeding with
pear seed—keep yourpear://link online so peers can fetch releases.