Understanding PeerHandle and ConversationID in BitChat: A Technical Guide

PeerHandle and ConversationID in BitChat provide a type-safe abstraction that separates stable conversation identity from ephemeral transport addressing, enabling seamless private chats even when peers rotate between temporary and permanent network identifiers.

BitChat, the open-source peer-to-peer messaging framework from permissionlesstech/bitchat, relies on these two core types to route messages across mesh, geohash, and direct channels. Understanding PeerHandle and ConversationID in BitChat is essential for developers extending the transport layer or debugging conversation state management.

What is ConversationID?

ConversationID is a Swift enum defined in bitchat/App/AppArchitecture.swift (lines 44-68) that acts as a stable key for message grouping and routing decisions. It distinguishes three distinct communication scopes, each mapping to a specific transport capability.

The Three Conversation Scopes

  • mesh — Represents the public broadcast channel visible to all nearby devices. The system routes these messages using TransportConfig.meshCap.

  • geohash(String) — Identifies location-based channels via a geohash string (e.g., "u4pruyd"). This case triggers TransportConfig.geohashCap for geographic routing.

  • direct(PeerHandle) — Encapsulates a private one-to-one conversation. Selecting this case activates TransportConfig.privateChatCap and carries a PeerHandle payload that contains the addressing logic.

What is PeerHandle?

PeerHandle is the concrete type backing the direct conversation case. Defined alongside ConversationID in AppArchitecture.swift, it stores two identifiers with distinct responsibilities:

  • id — A canonical string (e.g., "peer:abcd1234") that uniquely identifies the conversation itself. This value remains constant regardless of network changes.

  • routingPeerID — The raw PeerID that the transport layer uses to address the remote device on the network. This may change if a peer switches between ephemeral and stable identifiers.

Identity Stability Through Custom Hashing

The struct conforms to Equatable and Hashable, but its implementation deliberately ignores routingPeerID. As implemented in the source code, equality and hashing operate only on the canonical id. This design choice ensures that a direct conversation maintains a single identity in the UI and storage layer, even when the underlying transport address changes.

How BitChat Routes and Stores Messages

Transport Capability Mapping

In bitchat/App/ConversationStore.swift (lines 801-804), the system maps ConversationID cases to transport capabilities:

// Simplified logic from ConversationStore.swift
switch conversationID {
case .mesh: return transport.meshCap
case .geohash: return transport.geohashCap  
case .direct: return transport.privateChatCap
}

This mapping allows the networking layer to select the appropriate delivery mechanism based solely on the conversation type.

Message Storage by Conversation ID

ConversationStore indexes all messages using ConversationID as the dictionary key. For direct conversations, it provides an additional lookup mechanism via directMessagesByRoutingPeerID(), enabling the system to retrieve messages using the current transport-level peer ID even when the canonical conversation ID differs.

Practical Implementation Examples

Creating a Direct Conversation ID

The directPeer helper in ConversationStore.swift (lines 18-20) constructs a properly formatted ConversationID from a raw PeerID:

let peerID = PeerID(str: "peer-a")                 // Transport-level identifier
let directID = ConversationID.directPeer(peerID)   // → .direct(PeerHandle)

Under the hood, this factory method creates a PeerHandle with a canonical id formatted as "peer:\(peerID.id)" while preserving the original routingPeerID for network operations.

Storing and Retrieving Messages

// Append a message to a specific conversation
store.append(message, to: directID)

// Retrieve all messages for a conversation
let messages = store.messages(for: directID)

Lookup by Routing Peer ID

When handling network events where only the current transport ID is known:

let byPeer = store.directMessagesByRoutingPeerID()
let messagesForPeer = byPeer[peerID] ?? []

This pattern allows the UI to maintain conversation continuity—showing a single chat thread—while the networking layer handles addressing through potentially transient peer identifiers.

Summary

  • PeerHandle separates conversation identity (id) from network addressing (routingPeerID), enabling stable direct chats across peer ID changes.
  • ConversationID is a type-safe enum that distinguishes between mesh, geohash, and direct channels, driving transport capability selection.
  • The Equatable implementation of PeerHandle ignores routingPeerID, ensuring that storage and UI layers treat a conversation as consistent even when the transport layer sees different peer addresses.
  • ConversationStore in bitchat/App/ConversationStore.swift provides dual lookup mechanisms: by canonical ConversationID for UI consistency, and by routingPeerID for network event handling.

Frequently Asked Questions

What is the difference between PeerHandle.id and routingPeerID?

id is a canonical string that serves as the stable conversation identifier (e.g., "peer:abcd1234"), used for UI consistency and message storage. routingPeerID is the ephemeral transport-level PeerID actually used to send packets across the network. The separation allows a peer to change its network address without breaking the conversation thread.

How does BitChat route messages to different conversation types?

The system uses the ConversationID enum cases to select transport capabilities. According to ConversationStore.swift, ConversationID.mesh triggers TransportConfig.meshCap, geohash triggers geohashCap, and direct triggers privateChatCap, allowing the same message API to work across broadcast, geographic, and unicast channels.

Why does PeerHandle ignore routingPeerID in its hash value?

PeerHandle hashes only on its canonical id to ensure that two instances representing the same logical conversation—but with different transport addresses (such as ephemeral vs. stable peer IDs)—are treated as identical in Sets and Dictionary keys. This prevents the UI from fragmenting a single chat into multiple threads when a peer's network identity changes.

How are direct messages retrieved by routing peer ID?

ConversationStore provides the directMessagesByRoutingPeerID() method, which returns a dictionary mapping raw PeerID instances to their message arrays. This allows the transport layer to look up relevant messages when it only knows the current network address, while the canonical ConversationID maintains the persistent conversation view.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →