LogoPear Docs
How ToConnect to peers

Host multiple rooms in one chat app

Extend the pear-chat scaffold from a single room to an account that owns and joins many rooms, each with its own Autobase.

This guide shows you how to extend pear-chat from a single room to an account model that owns and joins many rooms. The reference implementation is pear-chat-multi-rooms.

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 scaffold is explained in the Reshape into a production app tutorial — read it first.

Before you begin

  • A working clone of pear-chat (or your own app built from the getting-started path).
  • Familiarity with Autobase — each room is one Autobase.

What changes

LayerChange
WorkerIntroduce a ChatAccount that holds a map of ChatRooms keyed by a generated room id.
SchemaAdd a room collection persisting the rooms a user has joined ({ id, name, invite, info }).
TransportAdd add-room and join-room message types, push a rooms event, and make the messages event and add-message command carry a roomId.
RendererAdd 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

Replace room with a ChatAccount on WorkerTask

In workers/worker-task.js, swap the single ChatRoom for a ChatAccount. The worker keeps the same Corestore + Hyperswarm setup, but delegates all room management to the account and forwards its messages event over the worker pipe as JSON (tagged with the roomId). _open opens the account — which loads any previously-joined rooms and instantiates one ChatRoom per entry — then handles the add-room, join-room, and add-message messages arriving on the pipe. _close tears down account → swarm → store:

workers/worker-task.js
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.invite)
    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()

    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
    }))
    rooms.sort((a, b) => a.info.at - b.info.at)
    this.pipe.write(JSON.stringify({ type: 'rooms', rooms }))
  }
}

module.exports = WorkerTask

Build ChatAccount

Create workers/chat-account.js modelled on chat-room.js. The account is itself an 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)). addRoom and joinRoom spin up a new ChatRoom, then append the room metadata to the account base so it survives restarts:

workers/chat-account.js
  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 for the underlying mental model.

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. 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:

schema.js
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:

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

In renderer/index.html and renderer/app.js, split the layout into a left rail (the room list) and a right pane (the active room). The renderer sends { type: 'add-room', name } and { type: 'join-room', invite } over the worker pipe, renders the left rail from the { type: 'rooms', rooms } events the worker pushes, and the existing message send becomes { type: 'add-message', text, roomId }. Incoming { type: 'messages', roomId, messages } events are filed under their roomId, so the renderer just shows the selected room's messages.

Run it

npm run build
npm start -- --storage /tmp/multi-a --name alice

Type a room name and click Create, copy the printed invite, then in a second terminal:

npm start -- --storage /tmp/multi-b --name bob --invite <invite>

Bob'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 with its own pairing flow.

Where to go next

On this page