How BitChat Implements Location-Based Channels Using Nostr: A Technical Deep Dive
BitChat creates geographic chat rooms by encoding latitude-longitude pairs into geohashes and attaching them to Nostr events via a dedicated g tag, enabling decentralized location-based messaging without central servers.
BitChat is an iOS application that builds peer-to-peer chat functionality on the Nostr protocol. The app implements location-based channels by treating geographic coordinates as channel identifiers, allowing users to join chat rooms scoped to specific cities, blocks, or custom regions. According to the permissionlesstech/bitchat source code, this implementation relies on a combination of geohash encoding, specialized Nostr tags, and geographic relay discovery.
Understanding the Geohash Channel Architecture
At the core of BitChat's location-based system lies a hierarchical model that converts physical coordinates into string identifiers suitable for the Nostr protocol.
Channel Representation with GeohashChannel
The application models geographic channels using the GeohashChannel struct defined in [bitchat/Protocols/LocationChannel.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/LocationChannel.swift). This struct combines a precision level (city, block, or custom) with a geohash string that encodes the latitude and longitude.
// Build a city-level location channel
let channel = GeohashChannel(level: .city, geohash: "9q8yy")
let channelID = ChannelID.location(channel)
The ChannelID.location wrapper encapsulates the geohash channel and provides a type-safe way to pass geographic scope throughout the application layer. The UI renders these channels by combining the human-readable level (e.g., "City") with the truncated geohash, creating identifiable room names like "City: 9q8yy".
Hierarchical Precision Levels
BitChat supports multiple granularity levels through the GeohashChannelLevel enum. Each level corresponds to a different geohash precision, allowing users to chat in broad metropolitan areas or specific street blocks. This hierarchy enables the location-based channels to scale from dense urban environments to sparse rural coverage without modifying the underlying Nostr event structure.
Encoding Geographic Scope with Nostr Tags
Once a channel is defined, BitChat must communicate its geographic boundaries to Nostr relays and other clients. The protocol achieves this through a standardized tagging convention.
The g Tag Convention
When publishing messages to a location-based channel, BitChat injects a Nostr tag with the identifier g into the event's tags array. The tag value contains the raw geohash string that defines the channel's boundaries.
var event = NostrEvent(
kind: 1059, // BitChat private-envelope kind
content: encryptedPayload,
tags: [["g", channel.geohash]] // Geographic scope tag
)
This g tag acts as a filter key for relays and clients. By convention established in the BitChat protocol, any Nostr event carrying a g tag is considered part of a location-based channel rather than a global or user-to-user conversation. The [NostrRelayManager.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrRelayManager.swift) file handles the injection of this tag during the publishing phase.
Tag Extraction and Validation
On the receiving side, BitChat validates incoming events by extracting and matching the geohash tag. The test suite in [GeohashPresenceServiceTests.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/GeohashPresenceServiceTests.swift) demonstrates this extraction logic, parsing the g tag from the raw Nostr event to determine which geographic channel should display the message.
Relay Discovery and Geographic Routing
BitChat does not assume that all Nostr relays host all location-based channels. Instead, it implements a directory service that maps geohashes to specific relay URLs, optimizing message propagation for geographic locality.
Mapping Geohashes to Relay Sets
The GeoRelayDirectory class, implemented in [GeoRelayDirectory.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/GeoRelayDirectory.swift), maintains a mapping between geohash prefixes and sets of Nostr relay URLs. When a user selects a location-based channel, the application queries this directory to determine which relays are responsible for that geographic area.
let relays = GeoRelayDirectory.shared.relays(for: channel.geohash)
This design allows BitChat to shard chat traffic across multiple relays based on geographic regions, reducing global broadcast overhead and improving latency for local conversations.
Publishing to Location-Specific Relays
The NostrRelayManager coordinates the actual message transmission. After resolving the target relays through GeoRelayDirectory, the manager publishes the signed Nostr event only to the relevant geographic relay set. This targeted publication ensures that location-based channel messages do not flood unrelated relay infrastructure or appear in users' feeds for different regions.
await NostrRelayManager.shared.publish(event, to: relays)
The GatewayService provides a higher-level abstraction that handles envelope encryption before passing the payload to the relay manager, as demonstrated in [GatewayServiceTests.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/GatewayServiceTests.swift).
Subscribing and Rendering Location-Scoped Messages
Client-side filtering ensures that users only see messages relevant to their currently selected geographic scope. When BitChat receives a Nostr event from a subscribed relay, it inspects the g tag and compares the value against the active ChannelID.location.
func handle(event: NostrEvent) {
guard let incomingGeohash = event.tags.first(where: { $0.first == "g" })?[1] else {
return // Event lacks geographic scope
}
if incomingGeohash == currentChannel.geohash {
displayMessage(event) // Render in UI
}
}
This client-side validation acts as a final safety layer, ensuring that even if a relay returns out-of-scope events, the UI filters them before display. The binding between the data model (GeohashChannel) and the view layer occurs through the ChannelID.location wrapper, maintaining a clean separation between Nostr protocol details and user interface code.
Summary
- Geohash Encoding: BitChat converts geographic coordinates into
GeohashChannelobjects with precision levels, wrapped inChannelID.locationfor type safety. - Nostr Tagging: Location-based channels use a dedicated
gtag containing the geohash string to scope events geographically on the Nostr protocol. - Relay Directory:
GeoRelayDirectorymaps geohashes to specific relay URL sets, enabling geographic sharding of message traffic. - Selective Publishing:
NostrRelayManagerpublishes channel messages only to relays responsible for the relevant geographic area, optimizing network efficiency. - Client Filtering: Incoming events are validated against the current
ChannelID.locationby extracting thegtag, ensuring users only see messages from their selected location-based channels.
Frequently Asked Questions
How does BitChat determine which Nostr relays to use for a specific location?
BitChat consults the GeoRelayDirectory service, which maintains a mapping between geohash strings and arrays of relay URLs. When a user joins a location-based channel, the application calls relays(for: channel.geohash) to resolve the specific set of relays that host that geographic region, then subscribes and publishes exclusively to that set.
What is the purpose of the g tag in BitChat's Nostr implementation?
The g tag encodes the geographic scope of a message. Its value contains the geohash string (e.g., ["g", "9q8yy"]) that identifies which location-based channel the event belongs to. Relays and clients use this tag to route and filter messages without parsing encrypted content, enabling efficient geographic distribution on the Nostr protocol.
Can users create location-based channels at different precision levels?
Yes. The GeohashChannelLevel enum in LocationChannel.swift supports multiple granularity levels including city and block precision. Users select their desired precision when creating a channel, which determines the length and specificity of the geohash string used in the g tag and relay discovery process.
How does BitChat prevent users from spoofing their location to access restricted channels?
BitChat relies on client-side verification of the g tag and trust-minimized relay assignment through GeoRelayDirectory. While the protocol itself does not cryptographically prove physical location (as GPS coordinates are self-reported), the relay directory can implement proof-of-location mechanisms or centralized verification to restrict which geohashes specific relays will accept, creating软性 boundaries for location-based channels.
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 →