LogoPear Docs
How ToStore and replicate

Add blind peering to a chat app

Keep a chat room reachable when none of its writers are online by attaching a blind-peering relay on top of the pear-chat scaffold.

This guide shows you how to layer blind peering on top of pear-chat so the room stays reachable even when none of its writers are connected. The reference implementation is pear-chat-blind-peering.

This guide is about the Pear-end, not the shell. The code below lives in the Bare worker — 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 for the cross-platform model and current support.

This is a delta-only how-to. The shared Electron + PearRuntime + Bare worker scaffold — with plain-JSON messages over a framed-stream pipe and a vanilla HTML renderer — is built step by step in Reshape into a production app — read that first.

Before you begin

What changes

LayerChange
DependenciesAdd blind-peering.
Worker entrypointDeclare a --blind-peer-key flag in workers/index.js so the key(s) reach the worker.
WorkerInstantiate a BlindPeering client against the swarm, point it at one or more blind-peer public keys, register the room's Autobase, and tear it down before the room.
RendererOptional: 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

Add the dependency

npm install blind-peering

Declare the --blind-peer-key flag

The worker only sees the flags workers/index.js declares with paparam. Add --blind-peer-key to the command(...) block so the key(s) you pass on the command line reach WorkerTask — without this, the runtime bails with UNKNOWN_FLAG: blind-peer-key:

workers/index.js
 const cmd = command('pear-chat',
+  flag('--blind-peer-key|-b <blindPeerKey>', 'Blind peer key').multiple(),
   flag('--invite|-i <invite>', 'Room invite'),
   flag('--name|-n <name>', '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

In workers/worker-task.js, instantiate BlindPeering against the swarm and a 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):

workers/worker-task.js
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, this.store.namespace('blind-peering'), {
      mirrors: 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 without holding its read key. Point the client at blind-peer keys you run yourself (the blind-peer CLI prints Listening at <key> on startup) or shared ones. See Availability and blind peering for the threat model (a blind peer replicates encrypted data but never holds the room encryption key).

See 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

The order in _close matters (L52–L57): the room holds 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 (L53), then the room, then the swarm, then the store (L54–L56).

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

Run one with blind-peer-cli (or point at a shared one you trust). On startup it logs a Listening at <key> line — copy that key, you pass it to both users as --blind-peer-key.

npm install -g blind-peer-cli
blind-peer
# {"level":30, ... ,"msg":"Listening at es4n7ty45odd1udfqyi9xz58mrbheuhdnxgdufsn9gz6e5uhsqco"}

2. Start user1 (creates the room)

Pass the blind-peer key so the room's Autobase is mirrored. The app prints an Invite: line on stdout — copy it for user2:

npm run build
npm start -- --storage /tmp/bp-user1 --name user1 --blind-peer-key <BLIND_PEER_KEY>

3. Start user2 (joins the room). Pass the invite from user1 and the same blind-peer key:

npm start -- --storage /tmp/bp-user2 --name user2 --invite <INVITE> --blind-peer-key <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

On this page