# A production Autobase-backed chat room



[`pear-chat`](https://github.com/holepunchto/pear-docs/tree/preview/examples/getting-started/pear-chat) is a real, production-shaped multi-writer chat room—the same scaffold [Reshape into a production app](/p2p/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app) wires into an Electron shell, and the base every "Extend your chat app" how-to modifies. This page traces the worker-side P2P code itself: a [Corestore](/p2p/reference/helpers/corestore) + [Hyperswarm](/p2p/reference/building-blocks/hyperswarm) + [Autobase](/p2p/reference/building-blocks/autobase) worker task, a `ChatRoom` built from Autobase and blind-pairing, and the generated schema that keeps them typed. None of this is Pear-specific—the same code runs identically in any Bare process.

<Callout type="info">
  Clone the repo to follow along:

  ```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
  ```

  For the Electron shell, preload bridge, and OTA updater wiring around this worker, see [Reshape into a production app](/p2p/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app).
</Callout>

## 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 its host over a framed pipe:

```js file=<rootDir>/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 a canonical shape for this kind of worker:

* **Construct (L9–L29)**—wire dependencies, do not perform I/O. The [Corestore](/p2p/reference/helpers/corestore), [Hyperswarm](/p2p/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 host IPC (JSON parsed off the pipe, L35–L45), then push the room's invite (L48) and the first batch of messages (L49) back.
* **`_peers()` (L58–L61)**—every connect/disconnect writes a `{ type: 'peers', count }` frame.
* **`_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 a bare-minimum worker are the **peer counter** and the **invite push** (L48)—the worker hands its host the room code so a UI can show a working "Copy invite" control.

## Read `ChatRoom` [#read-chatroom]

`workers/chat-room.js` is the actual peer-to-peer data structure. It combines four building blocks:

* An [Autobase](/p2p/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](/p2p/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:

```js file=<rootDir>/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=<rootDir>/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=<rootDir>/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=<rootDir>/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 its host.

<Callout type="info">
  For the deeper picture of Autobase merging, read [From append-only logs to files](/p2p/explanation/from-logs-to-files).
</Callout>

## 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=<rootDir>/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=<rootDir>/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](/p2p/reference/building-blocks/autobase) append-payload encoders, one per record type:

```js file=<rootDir>/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](/bare/explanation/rpc-and-schemas#schema-first-design).

## See also [#see-also]

* [Reshape into a production app](/p2p/getting-started/build-a-peer-to-peer-chat/reshape-into-a-production-app)—the Electron shell, preload bridge, and OTA updater built around this worker.
* [Structured RPC and schema-first design](/bare/explanation/rpc-and-schemas)—why schema-first design matters for peer-to-peer apps.
* [Add blind peering to a chat app](/p2p/how-to/blind-peering/add-blind-peering-to-a-chat-app)—keep this room online when its writers are offline.
* [Host multiple rooms in one chat app](/p2p/how-to/connect-to-peers/host-multiple-rooms-in-one-chat-app)—extend from one room to an account with many.
* [Autobase reference](/p2p/reference/building-blocks/autobase)—the multi-writer log API.
