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.iowss://relay.primal.netwss://offchain.pubwss://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
-
Directory lookup – The
RelayDirectoryclass queriesassets/nostr_relays.csv(a CSV mapping relay hostnames to geohash coordinates) to find the n nearest relays for a given geohash viaclosestRelaysForGeohash(geohash, nRelays). -
Caching – Results are stored in
geohashToRelays[geohash]to avoid repeated lookups. -
Default merging – When
includeDefaultsis 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_MULTIPLIERper 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 transmissionNostrEventDeduplicator– 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
RelayDirectoryoptimizes for geographic proximity, with optional default merging - Connection resilience through exponential back‑off and automatic subscription restoration
- Privacy enforcement via
LiveLocationPrivacyGatetoken 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →