# How Bitchat Android Coordinates Geohash Topic Channels Over Nostr

> Discover how Bitchat Android coordinates geohash topic channels over Nostr. Learn how it maps geohashes to Nostr topic channels, filters events, and routes messages efficiently.

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

---

**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`](https://github.com/permissionlesstech/bitchat-android/blob/main/LocationChannel.kt). This encapsulates both the geohash string and its precision level through the `GeohashChannelLevel` enum.

```kotlin
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.

```kotlin
// 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:

1. Extracts the `geohash` tag from event tags
2. Performs case-insensitive matching against the subscribed geohash
3. Records participant metadata and transforms valid events into internal messages

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

```kotlin
// 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`](https://github.com/permissionlesstech/bitchat-android/blob/main/MessageManager.kt) implement the routing logic, while [`ChatScreen.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/ChatScreen.kt) (lines 204-219) and [`CommandProcessor.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/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.

```kotlin
// 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`](https://github.com/permissionlesstech/bitchat-android/blob/main/GeohashNostrPrivacyPolicyTest.kt), ensuring that users don't broadcast block-level precisions when they intend city-level participation.

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`app/src/main/java/com/bitchat/android/geohash/LocationChannel.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/geohash/LocationChannel.kt) | Defines `GeohashChannel` and `GeohashChannelLevel` precision mapping |
| [`app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt) | Central Nostr listener filtering events by geohash tag |
| [`app/src/main/java/com/bitchat/android/nostr/GeohashRepository.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/nostr/GeohashRepository.kt) | Stores current geohash and participant metadata |
| [`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) | Coordinates subscription, UI state, and notifications |
| [`app/src/main/java/com/bitchat/android/ui/MessageManager.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/ui/MessageManager.kt) | Routes `"geo:"` prefixed messages to appropriate conversations |
| [`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) | Manages per-geohash notification suppression |
| [`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) | Displays messages from selected geohash channels |
| [`app/src/main/java/com/bitchat/android/nostr/GeohashNostrPrivacyPolicy.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/nostr/GeohashNostrPrivacyPolicy.kt) | Enforces geohash broadcasting privacy rules |
| [`app/src/main/java/com/bitchat/android/ui/GeohashPickerActivity.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/ui/GeohashPickerActivity.kt) | UI for selecting geohash precision levels |

## Summary

- **GeohashChannel** pairs a geohash string with precision level, defining the geographic scope of a conversation
- **Nostr subscription** filters events by `geohash` tag case-insensitively in `GeohashMessageHandler`
- **`"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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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.