How Bitchat Implements Location-Based Channels Using Geohash Coordinates with Nostr Relays
Bitchat maps geohash strings to deterministic channel identifiers and routes Nostr events through geohash-specific relay sets using carrier packets that enforce the #g tag constraint.
The open-source Bitchat protocol (permissionlesstech/bitchat) treats geographic areas as first-class communication channels by encoding location into short geohash strings. This design allows the network to partition message traffic across geographically distributed Nostr relays while maintaining hierarchical granularity from city blocks to individual buildings.
Core Data Model: GeohashChannel and Granularity Levels
The GeohashChannel Struct
At the heart of the implementation is the GeohashChannel struct defined in bitchat/Protocols/LocationChannel.swift. This lightweight value type conforms to Codable, Equatable, Hashable, and Identifiable, making it suitable for SwiftUI state management and network serialization.
struct GeohashChannel: Codable, Equatable, Hashable, Identifiable {
let id = UUID()
let level: GeohashChannelLevel // .region, .city, .building, …
let geohash: String // e.g., "u4pruydq"
}
The geohash property serves as the canonical channel identifier. Because geohashes are inherently hierarchical—dropping characters from the right expands the geographic area—the same coordinate can generate channels at multiple granularities without duplication.
Granularity Levels with GeohashChannelLevel
The GeohashChannelLevel enum, co-located in LocationChannel.swift, assigns semantic meaning to geohash precision. Each case knows its character count, allowing the UI to display appropriate zoom levels and the network layer to select correct relay sets.
enum GeohashChannelLevel {
case region // ~2 characters
case city // ~4 characters
case neighborhood // ~6 characters
case building // ~8+ characters
}
When a user selects a city-level view, Bitchat instantiates GeohashChannel(level: .city, geohash: "9q8yy"), truncating the full precision geohash to the appropriate length for that granularity.
Relay Discovery and Network Topology
Mapping Geohashes to Relay Endpoints
Bitchat decouples channel identity from physical infrastructure through a relay lookup function. The GeohashPresenceService accepts a closure that maps (geohash: String, level: Int) to an array of WebSocket URLs. Test implementations in GeohashPresenceServiceTests.swift demonstrate the contract:
let relayLookup: (String, Int) -> [String] = { geohash, _ in
["wss://\(geohash).example"]
}
In production deployments, this lookup queries DNS SRV records or parses a local CSV cache (relays/online_relays_gps.csv). The script scripts/validate_georelays.py audits this mapping, ensuring that every geohash prefix resolves to reachable Nostr relays.
Validating Relay Mappings
The validation script checks for relay responsiveness and geographic consistency. A well-formed deployment requires that relays advertised for a specific geohash actually peer with the broader network for that region, preventing message silos.
Publishing and Subscribing to Location Channels
Wrapping Events in NostrCarrierPacket
Outgoing messages are encapsulated in NostrCarrierPacket within GatewayService. This carrier attaches the destination geohash metadata required for routing:
let event = NostrEvent(kind: .textNote, content: "Hello neighborhood!", tags: [["g", "9q8yy"]])
let packet = try NostrCarrierPacket(
direction: .toGateway,
geohash: "9q8yy",
event: event
)
try gatewayService.publish(packet)
The packet structure ensures that the transport layer knows which relay set to target without parsing the Nostr event JSON.
Enforcing Geohash Consistency
GatewayService validates that the carrier's geohash field matches the #g tag inside the wrapped Nostr event. If the values diverge, the packet is rejected before transmission. This integrity check, verified in GatewayServiceTests.swift, prevents accidental cross-posting between geographic channels.
Presence Tracking with GeohashPresenceService
The GeohashPresenceService manages subscriptions to the relay set returned by the lookup function. It surfaces active participants through GeohashPeopleList, enabling UI features like "nearby users" indicators. The service follows the specification outlined in docs/GeohashPresenceSpec.md, which defines how presence metadata is exchanged via Nostr kind: 2 (recommend server) events tagged with the channel geohash.
End-to-End Message Flow
The complete lifecycle of a location-based message demonstrates how these components integrate:
-
Channel Selection: The user selects a city-level channel, instantiating
GeohashChannel(level: .city, geohash: "9q8yy"). -
Relay Resolution: The lookup function returns
["wss://9q8yy.example"]based on the CSV or DNS configuration. -
Subscription:
GatewayServiceopens a NostrREQfilter to those relays, subscribing to events with the tag["g", "9q8yy"]. -
Publishing: When the user sends a message, Bitchat creates a
NostrCarrierPacketwith the geohash field set to "9q8yy" and the event containing the matching#gtag. -
Routing: The gateway publishes to the resolved relays. Recipients subscribed to the same geohash receive the event through their
GeohashPresenceServicesubscription. -
Rendering: The UI displays the message within the context of the
GeohashChannel, maintaining geographic isolation from other channels.
Production Relay Lookup Example:
func relays(for geohash: String) -> [String] {
let csvPath = Bundle.main.path(forResource: "online_relays_gps", ofType: "csv")!
return CSVParser.parse(csvPath)[geohash] ?? []
}
Subscribing to Updates:
gatewayService.subscribe(to: cityChannel.geohash) { event in
// Append event to the location-specific timeline
}
Summary
- GeohashChannel in
bitchat/Protocols/LocationChannel.swiftprovides the core data structure, combining a geohash string with a granularity level. - Relay lookup functions map geohashes to WebSocket endpoints, enabling geographic partitioning of the Nostr network.
- NostrCarrierPacket enforces routing metadata consistency between the carrier envelope and the event's
#gtag. - GeohashPresenceService handles subscription lifecycle and presence tracking according to the specification in
docs/GeohashPresenceSpec.md. - The validation script
scripts/validate_georelays.pyensures relay assignments match their advertised geographic coverage.
Frequently Asked Questions
What is a geohash and why does Bitchat use it for location channels?
A geohash is a Base32-encoded string representing a latitude/longitude coordinate with variable precision. Bitchat uses geohashes because they provide a compact, hierarchical addressing scheme that naturally supports geographic granularity. Longer strings represent smaller areas, allowing the protocol to define channels at city, neighborhood, or building levels using the same coordinate system.
How does Bitchat prevent messages from appearing in the wrong geographic channel?
The GatewayService validates that the geohash field in the NostrCarrierPacket matches the #g tag within the wrapped Nostr event. If these values differ, the service rejects the publish operation. This ensures that routing metadata remains consistent with the event's declared geographic scope, preventing misrouted messages.
Can users join multiple granularity levels simultaneously?
Yes. Because each granularity level generates a distinct GeohashChannel with a unique identifier (e.g., "9q" for region vs. "9q8yy" for city), users can subscribe to multiple channels concurrently. The GeohashPresenceService maintains separate subscription states for each geohash, allowing parallel participation in both broad regional and hyperlocal building-level conversations.
How are relays assigned to specific geographic areas?
Relay assignment is handled by a pluggable lookup function that maps geohash strings to WebSocket URLs. Deployments typically use a CSV file (relays/online_relays_gps.csv) or DNS SRV records to define these mappings. The scripts/validate_georelays.py tool audits this configuration, verifying that advertised relays are online and correctly mapped to their respective geohash prefixes.
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 →