How the BitChat Distributed Board System Functions: A Technical Deep Dive
BitChat's distributed board system is a peer-to-peer bulletin board where every device maintains a synchronized local copy of signed posts and tombstones, propagated through the mesh network without centralized servers.
This architecture enables censorship-resistant, location-aware messaging that works entirely offline while optionally bridging to Nostr. The implementation in the permissionlesstech/bitchat repository demonstrates a complete end-to-end distributed data store using Swift, Noise cryptography, and gossip-based synchronization.
Core Architecture: Three-Layer Design
The distributed board system splits responsibilities across three distinct layers. This separation keeps the UI responsive, ingestion reliable, and transport minimal.
| Component | Location | Primary Role |
|---|---|---|
| BoardManager | BoardManager.swift |
UI-facing coordinator for post creation, deletion, and Nostr bridging |
| BoardStore | BoardStore.swift |
Thread-safe persistent archive with quotas and pruning |
| MeshBoardBroadcasting | Transport.swift |
Low-level mesh transport for gossip propagation |
BoardManager: The UI Coordinator
BoardManager lives at bitchat/Services/Board/BoardManager.swift and serves as the single entry point for all board operations. It maintains a @Published var posts property that SwiftUI views bind to directly, ensuring reactive UI updates when the underlying store changes.
The manager handles four critical tasks:
- Post creation with content validation, signing, and broadcasting
- Tombstone generation for deletions
- Geohash-based filtering for location-aware display
- Nostr bridging for cross-network visibility
BoardStore: The Distributed Database
BoardStore at bitchat/Services/Board/BoardStore.swift implements the actual distributed semantics. Every device runs an identical instance that ingests, validates, and persists the same data structure—creating eventual consistency across the mesh.
Key capabilities include:
- Signature verification before acceptance (
BoardWire.verifySignature) - Expiry enforcement with
expiresAttimestamps - Quota management with per-author and global caps
- Atomic persistence to
~/Library/Application Support/board/posts.json
The store exposes two Combine subjects for reactive updates: postArrivals (new inbound packets) and didWipe (panic recovery events).
Mesh-Only Transport
Board traffic never traverses direct relay connections—the MeshBoardBroadcasting protocol restricts propagation to gossip sync only. This design choice prevents metadata leakage to infrastructure nodes and ensures the board remains truly peer-to-peer.
Data Model: Posts and Tombstones
The wire format defines two packet types in BoardPackets.swift (referenced as BoardWire.swift):
BoardPostPacket Structure
struct BoardPostPacket {
let postID: Data // 16 random bytes
let geohash: String // Empty for mesh-only posts
let content: String // Max ~256 bytes
let authorSigningKey: Data
let nickname: String // Truncated to nicknameMaxBytes
let createdAt: Date
let expiresAt: Date
let isUrgent: Bool
let signature: Data // Noise signature over signingBytes
}
BoardTombstonePacket Structure
struct BoardTombstonePacket {
let postID: Data
let authorSigningKey: Data
let deletedAt: Date
let retainUntil: Date // Tombstones expire too
let signature: Data
}
Both types implement signingBytes computed properties that produce canonical serialization for the Noise signature. The signing key comes from transport.noiseSignData, binding board identity to the device's mesh identity.
Ingestion Pipeline: How Packets Enter the Store
When BoardStore.ingest receives a packet—whether from radio, sync round, or local echo—it executes a six-stage validation pipeline:
- Signature verification — already completed upstream but double-checked
- Expiry check — reject posts past
expiresAt, tombstones pastretainUntil - Clock skew tolerance — validate against
Limits.clockSkewMs - Deduplication — ignore identical
postID+authorSigningKeypairs - Quota enforcement — apply per-author and global limits, evicting oldest first
- Persistence — atomically write to
BoardStore.defaultFileURL
This pipeline runs under an internal lock (ingestLocked) to maintain thread safety during concurrent mesh ingestion and local operations.
Reading Posts: Geohash-Based Queries
The BoardManager.posts(forGeohash:) method implements the primary read path. It filters the in-memory snapshot by location and applies urgency-based sorting:
func posts(forGeohash geohash: String) -> [BoardPostPacket] {
posts
.filter { $0.geohash == geohash }
.sorted {
if $0.isUrgent != $1.isUrgent {
return $0.isUrgent
}
return $0.createdAt > $1.createdAt
}
}
An empty geohash parameter returns mesh-only posts—the default board visible to all peers regardless of location.
Creating a Post: End-to-End Flow
The BoardManager.createPost method implements the full publication pipeline:
// Example: Create a location-tagged urgent notice
let manager = BoardManager(transport: meshTransport)
let succeeded = manager.createPost(
content: "Emergency shelter available at coordinates...",
geohash: "9q5c", // Precision ~5km
urgent: true, // Pins to top of feed
expiryDays: 7,
nickname: "CivicResponse"
)
The internal implementation:
- Truncates content to ~256 bytes and nickname to
BoardWireConstants.nicknameMaxBytes - Generates cryptographically random
postIDviaSecRandomCopyBytes - Builds canonical
signingBytesfor the packet - Signs with
transport.noiseSignData - Broadcasts via
boardTransport?.sendBoardPayload - Optionally bridges to Nostr through
publishToNostr
Nostr Bridging Mechanics
When a geohash is present, the manager creates a NIP-40 geohash-text-note and publishes to relays from GeoRelayDirectory. The returned event ID is stored in bridgedEventIDs[postID] for synchronized deletion.
Deleting Posts: Tombstone Propagation
Deletion uses BoardManager.deletePost and follows a specific protocol for distributed consistency:
// Example: Remove a post you authored
if let post = manager.posts.first(where: {
$0.authorSigningKey == transport.noiseSigningPublicKeyData()
}) {
let ok = manager.deletePost(post)
}
The method:
- Verifies local ownership via signing key comparison (
isOwnPost) - Constructs and signs a
BoardTombstonePacket - Broadcasts the tombstone through the mesh
- Issues NIP-09 deletion to any bridged Nostr relays
Tombstones are retained until retainUntil to ensure late-joining peers learn of deletions, then automatically pruned.
Gossip Synchronization: Mesh-Wide Consistency
The distributed board achieves eventual consistency through the gossip sync subsystem. The BoardStore.syncCandidates() method returns all valid signed packets for re-broadcast to new peers.
Key integration points from Sync/GossipSyncBoardTests.swift:
- Sync flag
.boardrequests only board packets during negotiation - All posts and tombstones are re-gossiped regardless of age (if still valid)
- No centralized sequencer or timestamp authority required
This design prioritizes availability over strict ordering—posts may arrive out of sequence, but signature verification and deduplication ensure correct final state.
Persistence and Recovery
On application launch, BoardStore executes a cold-start procedure:
- Reads
posts.jsonfromBoardStore.defaultFileURL - Decodes each entry using
BoardWirecodecs - Re-verifies signatures (defense against disk tampering)
- Re-ingests through
ingestLockedto rebuildpostsSnapshot
The didWipe Combine subject allows BoardAlertsModel to notify the user when catastrophic recovery occurs.
Complete Code Examples
Initialize and Configure
import Foundation
// BoardManager requires a mesh transport conforming to MeshBoardBroadcasting
let meshTransport: MeshBoardBroadcasting = /* your transport instance */
let boardManager = BoardManager(transport: meshTransport)
// Optional: inject into SwiftUI environment
// .environmentObject(boardManager)
Publish to Location-Specific Board
let geoHash = GeoCache.currentGeohash // e.g., "9q5ct" from GPS
let published = boardManager.createPost(
content: "Community meeting tonight at 7pm",
geohash: geoHash,
urgent: false,
expiryDays: 1,
nickname: "Neighbor"
)
if published, let post = boardManager.posts.first {
print("Post ID: \(post.postID.base64EncodedString())")
// Check if bridged to Nostr
if let nostrID = boardManager.bridgedEventIDs[post.postID] {
print("Nostr event: \(nostrID)")
}
}
Query Multiple Geohash Precisions
// Get posts for current location at different precision levels
let precise = boardManager.posts(forGeohash: "9q5ct") // ~1km
let nearby = boardManager.posts(forGeohash: "9q5c") // ~5km
let regional = boardManager.posts(forGeohash: "9q5") // ~30km
// Get mesh-only posts (no location, visible to all peers)
let meshWide = boardManager.posts(forGeohash: "")
Handle Incoming Posts Reactively
import Combine
// Subscribe to new arrivals for alert generation
boardManager.boardStore.postArrivals
.sink { newPost in
if newPost.isUrgent {
// Trigger notification
NotificationCenter.default.post(
name: .urgentBoardPostReceived,
object: newPost
)
}
}
.store(in: &cancellables)
Key Files Reference
| File | Purpose |
|---|---|
bitchat/Services/Board/BoardManager.swift |
UI coordinator, post lifecycle, Nostr bridging |
bitchat/Services/Board/BoardStore.swift |
Persistent archive, ingestion, quotas, sync |
bitchat/Services/Board/BoardAlertsModel.swift |
User notification generation |
bitchat/Protocols/BoardPackets.swift |
Wire format and signature verification |
bitchat/Transport/Transport.swift |
MeshBoardBroadcasting protocol definition |
bitchat/Nostr/GeoRelayDirectory.swift |
Geographic relay discovery for bridging |
bitchat/Nostr/NostrProtocol.swift |
NIP-40/NIP-09 event construction |
bitchatTests/Sync/GossipSyncBoardTests.swift |
Sync subsystem integration tests |
Summary
BitChat's distributed board system demonstrates practical decentralized messaging through several key design decisions:
- Mesh-only transport prevents infrastructure dependencies and metadata exposure
- Self-certifying posts with Noise signatures enable permissionless participation
- Tombstone-based deletion provides authenticated revocation without consensus
- Geohash tagging enables location-aware routing with optional Nostr bridging
- Quota and expiry policies control storage growth on resource-constrained devices
- Gossip synchronization achieves eventual consistency without central coordination
The implementation in permissionlesstech/bitchat provides a complete production reference for building similar distributed data stores on peer-to-peer networks.
Frequently Asked Questions
How does BitChat prevent spam on the distributed board without a central server?
Spam mitigation relies on multiple layers: cryptographic proof-of-work via valid Noise signatures, per-author post caps (maxPostsPerAuthor), global storage limits (maxPosts), content length restrictions (~256 bytes), and automatic expiry pruning. These constraints make large-scale abuse computationally expensive and storage-prohibitive while preserving open participation.
Can board posts be edited after publication?
No—the distributed board uses an append-only log with tombstone revocation. To "edit" a post, the author must delete the original via BoardManager.deletePost and create a new post with updated content. This design simplifies consistency guarantees across asynchronous mesh synchronization.
What happens when two devices create posts with the same geohash simultaneously?
Geohash collisions are expected and harmless. Each post has a globally unique postID generated from SecRandomCopyBytes, so identical geohashes simply appear in the same filtered view. The posts(forGeohash:) sort order prioritizes isUrgent flags, then recency, ensuring consistent display across all devices.
How does the board system handle devices that were offline during post creation?
Offline devices receive missing posts through gossip sync when they reconnect. The sync engine requests .board flagged packets from peers, and BoardStore.syncCandidates() provides all valid posts for re-broadcast. Tombstones propagate the same way, ensuring deleted posts eventually disappear from all copies.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →