# How Geohash-Based Conversations Are Managed in Bitchat Android: Complete Architecture Guide

> Discover how Bitchat Android manages geohash-based conversations using a GeohashChannel and LocationChannelManager. Learn the complete architecture for location-based messaging.

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

---

**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](https://github.com/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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

```kotlin
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`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/geohash/LocationChannelManager.kt)

### Observing Channel Changes in Compose UI

```kotlin
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`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt)

### Sending Messages with Geohash Tags

```kotlin
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`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/nostr/NostrTransport.kt)

### Smart Notification Suppression

```kotlin
// 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`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/ui/NotificationManager.kt)

## UI Components for Geohash Interaction

| Component | Purpose | File |
|-----------|---------|------|
| [`LocationChannelsSheet.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/LocationChannelsSheet.kt) | Bottom sheet for precision selection and channel switching | [`app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt) |
| [`GeohashPeopleList.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/GeohashPeopleList.kt) | Displays participants discovered within current geohash cell | [`app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/ui/GeohashPeopleList.kt) |
| [`ChatScreen.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/ChatScreen.kt) | Main chat UI reacting to `selectedLocationChannel` state | [`app/src/main/java/com/bitchat/android/ui/ChatScreen.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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.