How Geohash-Based Conversations Are Managed in Bitchat Android: Complete Architecture Guide
Bitchat Android manages geohash-based conversations by encoding the device's latitude and longitude into a hierarchical geohash string, wrapping it in a GeohashChannel data class, and routing all Nostr messages through a LocationChannelManager that persists the selected channel and drives both UI recomposition and notification routing.
This location-scoped chat system enables ephemeral, proximity-based messaging without exposing precise GPS coordinates. The implementation spans geospatial encoding, reactive state management, and Nostr protocol integration as implemented in permissionlesstech/bitchat-android.
Core Architecture: From Coordinates to Channel ID
Bitchat's geohash conversation system rests on a clear separation between geospatial computation and chat abstraction layers.
| Component | Responsibility | Source File |
|---|---|---|
Geohash object |
Encode/decode lat/lng to geohash strings, compute cell boundaries | app/src/main/java/com/bitchat/android/geohash/Geohash.kt |
GeohashChannel data class |
Pair a precision level with a geohash string | app/src/main/java/com/bitchat/android/geohash/GeohashChannel.kt |
ChannelID sealed class |
Type-safe discriminator: Mesh vs. Location(GeohashChannel) |
app/src/main/java/com/bitchat/android/geohash/ChannelID.kt |
LocationChannelManager |
Single source of truth for selected channel; persists to SharedPreferences |
app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt |
GeohashViewModel |
UI-layer exposure via StateFlow for Compose recomposition |
app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt |
ChatState |
Central mutable state holder including _selectedLocationChannel |
app/src/main/java/com/bitchat/android/ui/ChatState.kt |
NostrTransport |
Attaches geohash tags to outgoing events; filters inbound by location |
app/src/main/java/com/bitchat/android/nostr/NostrTransport.kt |
Precision Levels and Privacy
Bitchat uses configurable precision levels to trade granularity for privacy:
WORLD(~5,000 km): Global reachCOUNTRY(~1,250 km): National scopeCITY(~40 km): Metropolitan areaNEIGHBORHOOD(~5 km): Local districtBLOCK(~1 km): Street-level
Each level corresponds to a geohash string length (1–9 characters). Shorter strings = larger areas = more participants, less precise location exposure.
State Flow: How a Location Becomes an Active Channel
The geohash channel lifecycle follows a reactive pipeline from GPS acquisition to Nostr routing:
- Location acquisition —
FusedLocationProvidersupplies raw coordinates - Geohash encoding —
Geohash.encode(lat, lng, precision)produces the hash string - Channel construction —
GeohashChannel(precision, geohashString)+ChannelID.Location(...) - Selection persistence —
LocationChannelManager.select(channel)updates internalStateFlowand serializes toSharedPreferencesvia Gson - UI propagation —
GeohashViewModel.selectedLocationChannelexposes the Flow; Compose recomposes - Message routing —
NostrTransportadds["geohash", "<hash>"]tags to outgoing events - Inbound filtering — Messages lacking matching geohash tags are dropped or routed to different channels
Practical Implementation: Code Examples
Selecting a Geohash Channel Programmatically
import com.bitchat.android.geohash.Geohash
import com.bitchat.android.geohash.GeohashChannel
import com.bitchat.android.geohash.GeohashChannelLevel
import com.bitchat.android.geohash.ChannelID
fun joinChannelAtLocation(
locationChannelManager: LocationChannelManager,
latitude: Double,
longitude: Double,
precision: GeohashChannelLevel = GeohashChannelLevel.CITY
) {
// Encode coordinates to geohash string at desired precision
val geohashString = Geohash.encode(latitude, longitude, precision)
// Build the complete channel identifier
val geohashChannel = GeohashChannel(precision, geohashString)
val channelId = ChannelID.Location(geohashChannel)
// Update state flow and persist selection
locationChannelManager.select(channelId)
}
Source: LocationChannelManager.select() in app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt
Observing Channel Changes in Compose UI
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import com.bitchat.android.ui.GeohashViewModel
import com.bitchat.android.geohash.ChannelID
@Composable
fun ChatHeader(viewModel: GeohashViewModel) {
val selectedChannel by viewModel.selectedLocationChannel.collectAsState()
val title = when (selectedChannel) {
is ChannelID.Location -> {
val locationChannel = (selectedChannel as ChannelID.Location).channel
"#${locationChannel.geohash} (${locationChannel.level.name.lowercase()})"
}
ChannelID.Mesh -> "Nearby Mesh Chat"
}
Text(text = title)
}
Source: GeohashViewModel.selectedLocationChannel in app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt
Sending Messages with Geohash Tags
import com.bitchat.android.nostr.NostrTransport
import com.bitchat.android.ui.ChatState
import com.bitchat.android.geohash.ChannelID
fun sendLocationScopedMessage(
chatState: ChatState,
nostrTransport: NostrTransport,
messageText: String
) {
val currentChannel = chatState.selectedLocationChannel.value
val tags = mutableListOf<List<String>>()
// Attach geohash metadata when in location mode
if (currentChannel is ChannelID.Location) {
val geohash = currentChannel.channel.geohash
tags.add(listOf("geohash", geohash))
}
// Publish via Nostr with location tags
nostrTransport.publish(
content = messageText,
tags = tags
)
}
Source: NostrTransport.publish() checks ChannelID.Location in app/src/main/java/com/bitchat/android/nostr/NostrTransport.kt
Smart Notification Suppression
// Inside NotificationManager.showGeohashNotification()
fun shouldNotifyForGeohash(geohash: String): Boolean {
val isAppInBackground = !lifecycleRegistry.isResumed
val currentGeohash = (chatState.selectedLocationChannel.value as? ChannelID.Location)
?.channel?.geohash
// Suppress if user is actively viewing this exact geohash channel
return isAppInBackground || currentGeohash != geohash
}
Source: NotificationManager.showGeohashNotification() in app/src/main/java/com/bitchat/android/ui/NotificationManager.kt
UI Components for Geohash Interaction
| Component | Purpose | File |
|---|---|---|
LocationChannelsSheet.kt |
Bottom sheet for precision selection and channel switching | app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt |
GeohashPeopleList.kt |
Displays participants discovered within current geohash cell | app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt |
ChatScreen.kt |
Main chat UI reacting to selectedLocationChannel state |
app/src/main/java/com/bitchat/android/ui/ChatScreen.kt |
These components consume GeohashViewModel.selectedLocationChannel and trigger LocationChannelManager.select() on user interaction.
Summary
- Geohash encoding in
Geohash.kttransforms GPS coordinates into privacy-preserving location strings at configurable precision levels - Channel abstraction via
GeohashChannelandChannelID.Locationcreates a type-safe, serializable representation of location-scoped chats - Centralized state management through
LocationChannelManagerensures consistent channel selection across UI, notifications, and network layers - Reactive UI updates flow from
StateFlowin the manager throughGeohashViewModelto Compose components - Nostr protocol integration attaches
geohashtags to events, enabling decentralized routing without centralized geofencing - Notification intelligence suppresses alerts when the user actively views the matching geohash channel
Frequently Asked Questions
How does Bitchat prevent exposing exact GPS coordinates?
Bitchat never transmits raw latitude/longitude. The Geohash.encode() function in Geohash.kt quantizes coordinates into a hierarchical string (e.g., u4pru for ~5 km precision). Users select precision levels from WORLD to BLOCK, trading granularity for participant reach. The geohash string alone appears in Nostr event tags.
Can users switch geohash channels without moving physically?
Yes. The LocationChannelsSheet.kt UI allows manual precision selection and channel browsing. LocationChannelManager.select() accepts any ChannelID.Location, enabling users to "teleport" to distant geohash cells for information gathering or coordination. The app persists manual selections across restarts.
What happens to messages when a user changes geohash channels?
Messages remain scoped to their original geohash via Nostr tags. When you switch channels, the UI filters the message list to show only events matching the current selectedLocationChannel. Historical messages from previous locations are preserved in local storage but hidden from the active view until you re-select that channel.
How does the mesh network interact with geohash channels?
ChannelID.Mesh operates independently as a peer-to-peer discovery layer, while ChannelID.Location uses Nostr relays for broader reach. The app maintains both simultaneously: mesh for ultra-local offline communication, geohash channels for relay-backed regional chat. The ChatState class holds whichever channel is currently active for message composition.
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 →