How Bitchat Android Coordinates Geohash Topic Channels Over Nostr
Bitchat Android maps each geohash area to a distinct Nostr topic channel using "geo:<geohash>" identifiers, filters incoming events by geohash tags, and routes messages through a unified chat infrastructure alongside direct messages.
Bitchat Android enables location-based messaging by treating geographic areas as topic channels on the Nostr protocol. This architecture allows users to join conversations tied to specific locations without separate server infrastructure—leveraging Nostr's native publish/subscribe mechanism for decentralized, censorship-resistant communication. The implementation bridges geohash precision levels with Nostr event tags, creating a seamless location-to-channel mapping system.
Geohash Channel Architecture and Data Model
Location channels in Bitchat are built around the GeohashChannel data class defined in LocationChannel.kt. This encapsulates both the geohash string and its precision level through the GeohashChannelLevel enum.
data class GeohashChannel(val level: GeohashChannelLevel, val geohash: String)
The GeohashChannelLevel enum maps geohash string length to human-readable precision:
- 4 characters → Region (country/state scale)
- 5 characters → City
- 6 characters → Neighborhood
- 7 characters → Block level
This precision-based approach lets users choose their location granularity when joining a conversation, balancing local relevance with privacy exposure according to the source code in permissionlesstech/bitchat-android.
Subscribing to Geohash Channels Over Nostr
When a user selects a location through the UI, GeohashViewModel initiates the subscription flow through GeohashMessageHandler. The handler receives the target geohash string as subscribedGeohash and begins listening for matching Nostr events.
// GeohashViewModel.kt (excerpt)
private val geohashMessageHandler = GeohashMessageHandler(
application = app,
repo = geohashRepo,
addChannelMessage = { channel, msg -> messageManager.addChannelMessage(channel, msg) }
)
fun selectGeohashChannel(channel: GeohashChannel) {
// tell the handler to listen for events tagged with this geohash
geohashMessageHandler.subscribe(channel.geohash)
// remember the current geohash for UI / notification logic
repo.setCurrentGeohash(channel.geohash)
notificationManager.setCurrentGeohash(channel.geohash)
}
The subscription mechanism demonstrates how geohash topic channels over Nostr are established: the client subscribes to geohash-tagged events rather than maintaining persistent connections to location-specific servers.
Receiving and Filtering Nostr Events by Geohash Tag
The core coordination logic resides in GeohashMessageHandler.onEvent, which processes every incoming Nostr event. The handler performs three critical operations:
- Extracts the
geohashtag from event tags - Performs case-insensitive matching against the subscribed geohash
- Records participant metadata and transforms valid events into internal messages
// GeohashMessageHandler.kt (excerpt)
fun onEvent(event: NostrEvent, subscribedGeohash: String) {
val tagGeo = event.tags.firstOrNull { it[0] == "geohash" }?.get(1) ?: return
if (!tagGeo.equals(subscribedGeohash, ignoreCase = true)) return
// Record the participant for presence display
repo.updateParticipant(subscribedGeohash, event.pubkey, Date(event.createdAt * 1000L))
// Convert to an internal BitchatMessage
val msg = NostrEmbeddedBitChat.encodePMForNostrNoRecipient(
content = event.content,
authorPubkey = event.pubkey,
channel = "#$subscribedGeohash"
)
// Push into the UI channel "geo:<geohash>"
addChannelMessage("geo:$subscribedGeohash", msg)
}
Line 46 in GeohashMessageHandler.kt performs the critical filtering step, while line 65 updates participant tracking through GeohashRepository. The event transformation on line 103 creates the bridge between raw Nostr events and Bitchat's internal message format.
Message Routing and UI Integration
Bitchat Android uses consistent "geo:<geohash>" identifiers throughout its routing layer. MessageManager and MeshDelegateHandler recognize this prefix pattern to direct messages to appropriate conversation views.
// ChatScreen.kt (excerpt)
val geokey = "geo:${locationChannel.channel.geohash}"
val messages = chatState.getChannelMessagesValue()[geokey] ?: emptyList()
LazyColumn {
items(messages) { message -> ChatMessageRow(message) }
}
Lines 65-66 in MessageManager.kt implement the routing logic, while ChatScreen.kt (lines 204-219) and CommandProcessor.kt (line 449) construct matching keys when sending or displaying messages. This unified routing allows geohash channels to coexist with direct message channels in the same conversation infrastructure.
Sending Messages to Geohash Channels
Outbound message flow follows the reverse path: constructing Nostr events with geohash tags and publishing through the relay manager.
// GeohashViewModel.kt (excerpt)
fun sendGeohashMessage(
content: String,
channel: GeohashChannel,
myPeerID: String,
nickname: String?
) {
// Build a Nostr event with a geohash tag
val nostrEvent = NostrEmbeddedBitChat.encodePMForNostr(
content = content,
authorPubkey = myPeerID,
channel = "#${channel.geohash}",
nickname = nickname
)
// Publish via the Nostr relay manager
NostrRelayManager.shared.publish(nostrEvent)
}
The channel parameter receives the "#<geohash>" format (with hash prefix) for proper Nostr tag structure, while the internal routing uses "geo:<geohash>" for UI consistency.
State Management and Notification Coordination
Active channel tracking prevents duplicate notifications and maintains UI coherence. GeohashRepository stores the currently viewed geohash through setCurrentGeohash (line 70), synchronized with NotificationManager (lines 174-176) to suppress alerts when the user is actively viewing that channel.
This coordination ensures that geohash topic channels over Nostr behave like standard chat conversations from the user perspective, with proper focus-aware notification handling.
Privacy Controls for Geohash Broadcasting
Not all geohash precisions are suitable for live presence broadcasting. GeohashNostrPrivacyPolicy enforces rules determining which tags may be exposed to other peers, preventing accidental precise location leaks. The policy is validated through GeohashNostrPrivacyPolicyTest.kt, ensuring that users don't broadcast block-level precisions when they intend city-level participation.
Key Implementation Files
Summary
- GeohashChannel pairs a geohash string with precision level, defining the geographic scope of a conversation
- Nostr subscription filters events by
geohashtag case-insensitively inGeohashMessageHandler "geo:<geohash>"identifiers provide consistent routing across UI, messaging, and notification layers- Participant tracking enables presence display without centralized user registries
- Privacy policies prevent over-sharing of precise location data
- Unified infrastructure lets geohash channels coexist with direct messages using identical patterns
Frequently Asked Questions
How does Bitchat prevent users from joining incorrect geohash channels?
The GeohashMessageHandler performs strict case-insensitive matching of the geohash tag against the subscribed geohash string on line 46 of GeohashMessageHandler.kt. Events with non-matching tags are silently discarded, ensuring users only receive messages intended for their selected geographic area.
What geohash precision levels does Bitchat Android support?
Bitchat supports four precision levels through GeohashChannelLevel: 4-character (region), 5-character (city), 6-character (neighborhood), and 7-character (block). These map to increasingly specific geographic boundaries, letting users choose their conversational scope.
Can users receive notifications for multiple geohash channels simultaneously?
The NotificationManager tracks a single currentGeohash at a time (line 174-176), suppressing notifications only for the actively viewed channel. Other subscribed channels would generate notifications normally, though the current implementation focuses on one active geohash conversation similar to direct message behavior.
How does Bitchat handle location privacy for geohash broadcasting?
GeohashNostrPrivacyPolicy implements rules restricting which precision levels may be broadcast live to the Nostr network. This prevents users from accidentally exposing precise block-level locations when broader city or region-level participation would suffice for their conversational needs.
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 →