# Understanding PeerHandle and ConversationID in BitChat: A Technical Guide

> Discover the purpose of PeerHandle and ConversationID in BitChat. Learn how these elements ensure type-safe conversation identity and seamless private chats, even with changing network identifiers.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-23

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/App/ConversationStore.swift) (lines 801-804), the system maps `ConversationID` cases to transport capabilities:

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/ConversationStore.swift) (lines 18-20) constructs a properly formatted `ConversationID` from a raw `PeerID`:

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

```swift
// 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:

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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.