How Nostr Relays Are Selected and Managed in Bitchat Android Messaging

Bitchat Android uses a dedicated NostrRelayManager class that combines a static default relay pool with geohash‑based proximity selection, backed by exponential back‑off reconnection and live‑location privacy gating.

The Bitchat Android app implements a sophisticated, multi‑layered approach to Nostr relay handling. At its core, the NostrRelayManager in app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt orchestrates relay selection, connection lifecycle, and subscription management—ensuring messages reach the right relays while respecting user privacy.

Default Relay Pool Initialization

When NostrRelayManager is instantiated, it immediately populates an internal relaysList with four hard‑coded public relays. These are defined in the DEFAULT_RELAYS constant at lines 44‑49:

  • wss://relay.damus.io
  • wss://relay.primal.net
  • wss://offchain.pub
  • wss://nostr21.com

These URLs are always kept in the nonLiveRelayUrls set, making them the fallback for all non‑live‑location traffic. This ensures baseline connectivity even when geohash‑specific selection fails or is disabled.

Geohash‑Based Relay Selection

For location‑aware messaging, Bitchat Android selects relays based on geographic proximity using geohash coordinates.

How Geohash Selection Works

  1. Directory lookup – The RelayDirectory class queries assets/nostr_relays.csv (a CSV mapping relay hostnames to geohash coordinates) to find the n nearest relays for a given geohash via closestRelaysForGeohash(geohash, nRelays).

  2. Caching – Results are stored in geohashToRelays[geohash] to avoid repeated lookups.

  3. Default merging – When includeDefaults is true, the nearest relays are unioned with the default pool: (nearest + defaultRelays()).toSet().

The key method implementing this logic is ensureGeohashRelaysConnected (lines 45‑63 in NostrRelayManager.kt), which guarantees WebSocket connections exist before any message or subscription is attempted.

// Connect to 5 nearest relays for a geohash, plus defaults
NostrRelayManager
    .getInstance(context)
    .ensureGeohashRelaysConnected(
        geohash = "u4pruydqqvj",
        nRelays = 5,
        includeDefaults = true,
        liveLocationToken = token
    )

Public API for Geohash Operations

Method Purpose
ensureGeohashRelaysConnected(...) Forces connections for selected relays
getRelaysForGeohash(...) Returns current relay list for a geohash
sendEventToGeohash(event, geohash, ...) Sends event to geohash‑specific relays (with default fallback)
subscribeForGeohash(...) Creates filtered subscription tied to a geohash

Connection and Subscription Lifecycle

WebSocket Management

The manager creates OkHttp WebSocket connections on demand via connectToRelay(url, liveLocationToken). Reconnection jobs are tracked in reconnectJobs to prevent duplicate attempts.

Exponential Back‑Off Reconnection

When a non‑DNS disconnection occurs, handleCurrentDisconnection triggers exponential back‑off:

  • Base interval: INITIAL_BACKOFF_INTERVAL
  • Multiplier: BACKOFF_MULTIPLIER per attempt
  • Maximum cap: MAX_BACKOFF_INTERVAL

The retry delay is calculated as INITIAL_BACKOFF_INTERVAL * BACKOFF_MULTIPLIER^(attempt‑1). This logic appears around lines 335‑377 in NostrRelayManager.kt.

Subscription Persistence

After reconnection, restoreSubscriptionsForRelay automatically rebuilds all active subscriptions for that relay. Subscriptions are registered as SubscriptionInfo objects that record their originating geohash, ensuring they target the correct relay set (targetRelayUrls = relayUrls).

Privacy and Security Controls

Live Location Privacy Gating

Every network action checks LiveLocationPrivacyGate.accepts(token). If a token is revoked, revokeLiveLocationAccess drops all live‑location‑specific connections and clears related state. This prevents network leakage that could deanonymize users.

Event Deduplication and Queueing

Both sending and subscribing share:

  • NostrPendingEventQueue – Buffers events before transmission
  • NostrEventDeduplicator – Prevents duplicate processing

Complete Usage Examples

// Initialize and connect to all relays (default + cached geohash)
val manager = NostrRelayManager.getInstance(context)
manager.connect()

// Send a chat message to geohash-specific relays
val dmEvent = NostrEvent.create(
    kind = 4,
    content = encryptedContent,
    tags = listOf(listOf("p", recipientPubkey))
)

manager.sendEventToGeohash(
    event = dmEvent,
    geohash = "u4pruydqqvj",
    includeDefaults = true,
    nRelays = 5
)

// Subscribe to messages in a geohash region
val chatFilter = NostrFilter.Tags(listOf("chat"))

manager.subscribeForGeohash(
    geohash = "u4pruydqqvj",
    filter = chatFilter,
    includeDefaults = true,
    nRelays = 5,
    handler = { event ->
        Log.d("Bitchat", "New message: ${event.content}")
    }
)

Key Source Files

File Role
NostrRelayManager.kt Core manager: default relays, geohash selection, connections, subscriptions, reconnection logic
RelayDirectory.kt Proximity-based relay lookup from assets/nostr_relays.csv
assets/nostr_relays.csv Relay geolocation database for nearest‑neighbor queries

Summary

  • Default pool of four well‑known relays provides always‑available fallback connectivity
  • Geohash-based selection via RelayDirectory optimizes for geographic proximity, with optional default merging
  • Connection resilience through exponential back‑off and automatic subscription restoration
  • Privacy enforcement via LiveLocationPrivacyGate token validation before all network operations
  • Shared infrastructure of pending event queues and deduplicators ensures reliable message flow

Frequently Asked Questions

How does Bitchat Android choose which Nostr relay to use for a message?

The NostrRelayManager first determines if the message is associated with a geohash. If so, it queries RelayDirectory.closestRelaysForGeohash() to find the nearest relays from assets/nostr_relays.csv, optionally merges the default relay pool, and sends to that combined set. For non‑geohash messages, it uses the static default pool (wss://relay.damus.io, wss://relay.primal.net, wss://offchain.pub, wss://nostr21.com).

What happens if a Nostr relay connection drops in Bitchat Android?

The manager detects disconnections in handleCurrentDisconnection, increments relay.reconnectAttempts, and schedules a retry with exponential back‑off. Once reconnected, restoreSubscriptionsForRelay automatically resubscribes to all active filters for that relay. DNS failures are treated differently from transient errors to avoid unnecessary back‑off.

Can users disable geohash-based relay selection?

While the public API exposes includeDefaults to force fallback to the default pool, the underlying geohash lookup in RelayDirectory can be bypassed by calling methods without a geohash parameter or by setting nRelays such that only defaults are used. The privacy gate also allows runtime revocation of live‑location access, which drops geohash‑specific connections.

Where is the relay geolocation data stored?

Relay coordinates are stored in app/src/main/assets/nostr_relays.csv, a static CSV file bundled with the app. RelayDirectory.kt parses this file at runtime to compute nearest‑neighbor lookups for any given geohash.

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 →