How Bitchat Android Implements Store-and-Forward Messaging for Offline Peers
Bitchat Android guarantees message delivery to offline peers by buffering packets in a dedicated StoreForwardManager that uses separate caches for favorite and regular contacts and forwards them automatically when the recipient reconnects.
The permissionlesstech/bitchat-android mesh messenger must operate across intermittent BLE and Wi-Fi Aware links where peers regularly disconnect. Its store-and-forward mechanism solves this by persisting direct messages in memory until the destination peer is reachable again. The subsystem is implemented in app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt and is designed to be transport-agnostic.
Core Components of the Store-and-Forward System
The StoreForwardManager class controls caching, deduplication, and retransmission without blocking UI threads.
StoreForwardManager and Its Delegate
MeshCore.kt instantiates the manager as a singleton property:
private val storeForwardManager = StoreForwardManager()
The manager receives runtime dependencies through the StoreForwardManagerDelegate interface declared at lines 41–46 and consumed at lines 312–316 of StoreForwardManager.kt. The delegate exposes three methods:
isFavorite(peerID)— Returnstrueif the recipient is a favorite contact.isPeerOnline(peerID)— Reports real-time connectivity status.sendPacket(packet)— Executes the actual transmission.
This design keeps StoreForwardManager agnostic to whether the underlying transport is BLE, Wi-Fi Aware, or Tor.
Dual-Cache Architecture
The manager maintains two distinct in-memory stores to prioritize traffic and bound memory growth. Lines 35–38 of StoreForwardManager.kt define:
messageCache— A synchronized list ofStoredMessageobjects for regular peers, capped byMAX_CACHED_MESSAGES.favoriteMessageQueue— AConcurrentHashMapmapping peer IDs to dedicated queues for favorite contacts, capped byMAX_CACHED_MESSAGES_FAVORITES.deliveredMessages— A bookkeeping set that records message IDs already forwarded to prevent duplicates.
Separating favorites from regular peers ensures that high-priority contacts receive a larger buffer and faster delivery guarantees.
Periodic Cleanup
A Kotlin coroutine launched via startPeriodicCleanup wakes every CLEANUP_INTERVAL (10 minutes) on an IO dispatcher. It invokes two routines:
cleanupMessageCache()— Evicts non-favorite entries older thanMESSAGE_CACHE_TIMEOUT(approximately 12 hours).cleanupDeliveredMessages()— Shrinks thedeliveredMessagesset to prevent indefinite growth.
Because cleanup runs on a background thread, the manager never stalls the main mesh event loop.
Store-and-Forward Workflow for Offline Peers
The workflow is triggered by packet reception events inside MeshCore.kt and completes when the target peer reconnects.
Arrival and Filtering
When the mesh layer receives a packet, MeshCore calls:
val messageId = UUID.randomUUID().toString()
storeForwardManager.cacheMessage(packet, messageId)
Inside cacheMessage (lines 55–68), the manager first discards message types that never require caching, such as handshakes and announcements. It then drops broadcast packets and validates that the recipientPeerID is present and well-formed. Only directed messages survive this filter.
Storage Routing
After validation, the manager queries its delegate via isFavorite(recipientPeerID) to choose a destination cache (lines 80–87):
- Favorite peer — The
StoredMessageis appended tofavoriteMessageQueue[recipientPeerID]. The queue is bounded byMAX_CACHED_MESSAGES_FAVORITES. - Regular peer — The message is added to the global
messageCache, bounded byMAX_CACHED_MESSAGES.
Both paths log the resulting cache size for observability (lines 89–115). If a cache is at capacity, the oldest entries are evicted before insertion.
Reconnect and Forwarding
When the mesh service detects that a peer has come online, it invokes sendCachedMessages(peerID) (lines 21–78). This routine performs three steps:
- Gathering — Collects all queued favorite messages for the peer and all regular
messageCacheentries whoserecipientIDmatches. Already-delivered IDs present indeliveredMessagesare excluded. - Sorting — The surviving packets are ordered by their original timestamps so that conversation history is relayed sequentially (lines 55–57).
- Transmission — Each packet is emitted through
delegate?.sendPacket(storedMessage.packet)with a 10 ms inter-message delay to avoid link saturation (lines 66–70). After successful dispatch, the message ID is recorded indeliveredMessagesand the entry is removed from its cache (lines 62–74).
This sequence ensures eventual delivery while preserving causal order.
Background Maintenance
Even if a peer never returns, the manager does not leak memory. The background coroutine repeatedly scrubs stale data, enforcing the 12-hour lifetime limit on regular cached messages and compacting the deduplication set.
Integration with the Mesh Transport Layer
The delegate pattern makes StoreForwardManager reusable across transports. In BluetoothMeshService.kt, the delegate implements isPeerOnline by checking the current BLE GATT connection table, while WifiAwareMeshService.kt uses Wi-Fi Aware session state. Both services share the same manager instance wired through MeshCore.kt, proving that the store-and-forward logic is completely decoupled from radio-specific code.
The following snippet shows the typical API surface used by transport implementations:
// 1️⃣ Cache a new outgoing packet (called from MeshCore)
val packet = BitchatPacket(/* … */)
val messageId = UUID.randomUUID().toString()
storeForwardManager.cacheMessage(packet, messageId)
// 2️⃣ When a peer reconnects, forward any pending packets
storeForwardManager.sendCachedMessages(peerId)
// 3️⃣ Query how many messages are waiting for a specific peer
val pending = storeForwardManager.getCachedMessageCount(peerId)
Log.d("SF", "Pending messages for $peerId: $pending")
Summary
StoreForwardManager.ktimplements the complete store-and-forward lifecycle: caching, deduplication, forwarding, and cleanup.- The manager relies on
StoreForwardManagerDelegateto query peer favorites, online status, and to send packets, remaining transport-agnostic. - Messages are split between a regular
messageCacheand a per-peerfavoriteMessageQueue, each with hard capacity limits. - A periodic coroutine cleans up entries older than 12 hours and shrinks the
deliveredMessagesdeduplication set every 10 minutes. - On peer reconnect, cached packets are sorted by timestamp and forwarded with a 10 ms pacing delay to prevent flooding.
Frequently Asked Questions
How does Bitchat Android prevent duplicate messages when a peer reconnects?
The manager maintains a deliveredMessages set that records every message ID successfully forwarded. Before transmitting a cached packet, sendCachedMessages filters out any ID already present in this set, guaranteeing exactly-once delivery semantics for stored packets.
What is the difference between favorite and regular peer message caching?
Favorite peers receive a dedicated ConcurrentHashMap queue with a high limit (MAX_CACHED_MESSAGES_FAVORITES), while regular peers share a single synchronized messageCache capped at MAX_CACHED_MESSAGES. This prioritization ensures that messages to important contacts survive longer and experience less eviction pressure.
How long does Bitchat Android keep undelivered messages?
Regular cached messages are evicted after MESSAGE_CACHE_TIMEOUT, approximately 12 hours, by the periodic cleanup coroutine. Favorite queues are not subject to the same age-based eviction, though they remain bounded by their own capacity limit.
Which Kotlin class manages store-and-forward for offline peers?
The entire mechanism is centralized in StoreForwardManager, defined in app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt. It is instantiated by MeshCore.kt and driven by delegate callbacks supplied from BluetoothMeshService.kt and WifiAwareMeshService.kt.
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 →