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:

  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
// 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

File Purpose
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 Central Nostr listener filtering events by geohash tag
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 Coordinates subscription, UI state, and notifications
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 Manages per-geohash notification suppression
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 Enforces geohash broadcasting privacy rules
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. 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:

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 →