# How the BitChat Distributed Board System Functions: A Technical Deep Dive

> Explore the BitChat distributed board system. Discover how this peer-to-peer bulletin board synchronizes signed posts and tombstones across its mesh network without central servers.

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

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/BoardManager.swift) | UI-facing coordinator for post creation, deletion, and Nostr bridging |
| **BoardStore** | [`BoardStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BoardStore.swift) | Thread-safe persistent archive with quotas and pruning |
| **MeshBoardBroadcasting** | [`Transport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/Transport.swift) | Low-level mesh transport for gossip propagation |

### BoardManager: The UI Coordinator

`BoardManager` lives at [`bitchat/Services/Board/BoardManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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 `expiresAt` timestamps
- **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`](https://github.com/permissionlesstech/bitchat/blob/main/BoardPackets.swift) (referenced as [`BoardWire.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BoardWire.swift)):

### BoardPostPacket Structure

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

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

1. **Signature verification** — already completed upstream but double-checked
2. **Expiry check** — reject posts past `expiresAt`, tombstones past `retainUntil`
3. **Clock skew tolerance** — validate against `Limits.clockSkewMs`
4. **Deduplication** — ignore identical `postID` + `authorSigningKey` pairs
5. **Quota enforcement** — apply per-author and global limits, evicting oldest first
6. **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:

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

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

1. Truncates content to ~256 bytes and nickname to `BoardWireConstants.nicknameMaxBytes`
2. Generates cryptographically random `postID` via `SecRandomCopyBytes`
3. Builds canonical `signingBytes` for the packet
4. Signs with `transport.noiseSignData`
5. Broadcasts via `boardTransport?.sendBoardPayload`
6. 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:

```swift
// Example: Remove a post you authored
if let post = manager.posts.first(where: { 
    $0.authorSigningKey == transport.noiseSigningPublicKeyData() 
}) {
    let ok = manager.deletePost(post)
}

```

The method:

1. Verifies local ownership via signing key comparison (`isOwnPost`)
2. Constructs and signs a `BoardTombstonePacket`
3. Broadcasts the tombstone through the mesh
4. 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`](https://github.com/permissionlesstech/bitchat/blob/main/Sync/GossipSyncBoardTests.swift):

- Sync flag `.board` requests 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:

1. Reads [`posts.json`](https://github.com/permissionlesstech/bitchat/blob/main/posts.json) from `BoardStore.defaultFileURL`
2. Decodes each entry using `BoardWire` codecs
3. Re-verifies signatures (defense against disk tampering)
4. Re-ingests through `ingestLocked` to rebuild `postsSnapshot`

The `didWipe` Combine subject allows `BoardAlertsModel` to notify the user when catastrophic recovery occurs.

## Complete Code Examples

### Initialize and Configure

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

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

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

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/Board/BoardManager.swift) | UI coordinator, post lifecycle, Nostr bridging |
| [`bitchat/Services/Board/BoardStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/Board/BoardStore.swift) | Persistent archive, ingestion, quotas, sync |
| [`bitchat/Services/Board/BoardAlertsModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/Board/BoardAlertsModel.swift) | User notification generation |
| [`bitchat/Protocols/BoardPackets.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/BoardPackets.swift) | Wire format and signature verification |
| [`bitchat/Transport/Transport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Transport/Transport.swift) | `MeshBoardBroadcasting` protocol definition |
| [`bitchat/Nostr/GeoRelayDirectory.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/GeoRelayDirectory.swift) | Geographic relay discovery for bridging |
| [`bitchat/Nostr/NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift) | NIP-40/NIP-09 event construction |
| [`bitchatTests/Sync/GossipSyncBoardTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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.