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 reach
  • COUNTRY (~1,250 km): National scope
  • CITY (~40 km): Metropolitan area
  • NEIGHBORHOOD (~5 km): Local district
  • BLOCK (~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:

  1. Location acquisition — FusedLocationProvider supplies raw coordinates
  2. Geohash encoding — Geohash.encode(lat, lng, precision) produces the hash string
  3. Channel construction — GeohashChannel(precision, geohashString) + ChannelID.Location(...)
  4. Selection persistence — LocationChannelManager.select(channel) updates internal StateFlow and serializes to SharedPreferences via Gson
  5. UI propagation — GeohashViewModel.selectedLocationChannel exposes the Flow; Compose recomposes
  6. Message routing — NostrTransport adds ["geohash", "<hash>"] tags to outgoing events
  7. 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.kt transforms GPS coordinates into privacy-preserving location strings at configurable precision levels
  • Channel abstraction via GeohashChannel and ChannelID.Location creates a type-safe, serializable representation of location-scoped chats
  • Centralized state management through LocationChannelManager ensures consistent channel selection across UI, notifications, and network layers
  • Reactive UI updates flow from StateFlow in the manager through GeohashViewModel to Compose components
  • Nostr protocol integration attaches geohash tags 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →