Geohash-Based Location Channels in BitChat's Nostr Integration: A Complete Technical Guide
Geohash-based location channels in BitChat use compact base-32 geohash strings to scope Nostr relay subscriptions to specific geographic cells, enabling hyper-local messaging without centralized servers.
BitChat implements a novel location-aware messaging layer on top of the Nostr protocol by encoding latitude and longitude into hierarchical geohash cells. According to the permissionlesstech/bitchat source code, these channels allow users to broadcast and subscribe to conversations that are automatically restricted to specific geographic areas, from individual buildings to entire regions.
What Are Geohash-Based Location Channels?
A geohash-based location channel is a Nostr relay subscription scoped to a specific geohash cell—a deterministic, compact base-32 string representing a rectangular geographic area. Instead of broadcasting messages globally, BitChat users publish events tagged with their current geohash, allowing relays to filter and deliver only relevant, nearby content.
The system uses the standard Nostr #g tag to encode location data, enabling any compatible relay to support spatial filtering without proprietary extensions. This design creates privacy-preserving, local-only chat rooms where membership is determined purely by physical presence within a geohash boundary.
The Geohash Channel Hierarchy
BitChat organizes location into six distinct administrative levels, each mapping to a specific geohash precision (string length).
Channel Levels and Precision
In LocationChannel.swift, the GeohashChannelLevel enum defines the hierarchy:
| Level | Precision | Geohash Length | Coverage Area |
|---|---|---|---|
| building | 8 | 8 characters | Specific building |
| block | 7 | 7 characters | City block |
| neighborhood | 6 | 6 characters | Neighborhood |
| city | 5 | 5 characters | City |
| province | 3 | 3 characters | Province/State |
| region | 2 | 2 characters | Large region |
Higher precision values (8) yield smaller cells for intimate conversations, while lower values (2) create broader channels for regional announcements.
The GeohashChannel Struct
Each channel is represented by the GeohashChannel struct, which pairs a GeohashChannelLevel with the actual geohash string. The application exposes these through ChannelID.location(GeohashChannel), distinguishing them from the global .mesh channel used for worldwide broadcasts.
let cityChannel = GeohashChannel(level: .city, geohash: "u4pru")
let channelID = ChannelID.location(cityChannel)
LocationStateManager tracks the user's selected channel and coordinates UI updates when the device moves between cells.
Nostr Protocol Integration
BitChat extends standard Nostr events with geospatial metadata while maintaining full protocol compatibility.
Event Types and Tagging
The NostrProtocol.swift file provides three primary event constructors for geohash-based communication:
createEphemeralGeohashEvent generates kind-20000 events for transient public messages. These include the ["g", geohash] tag and optionally support nicknames or additional metadata tags.
createGeohashPresenceEvent creates kind-20001 heartbeat events with empty content. These announce a user's presence in a specific cell without carrying conversation data, enabling the "people nearby" feature.
createGeohashTextNote produces standard NIP-1 text notes (kind-1) tagged with a geohash, optionally including expiration timestamps for temporary announcements.
All three methods utilize the #g tag for standard geohash cells or #r tags for bridge-mesh cells, allowing relays to filter subscriptions efficiently.
Subscription and Routing
When subscribing to a channel, the BitChat client sends filter requests containing the target geohash tag. Because Nostr relays natively support tag-based filtering, the network delivers only events matching the user's current geographic cell or explicitly specified neighbors. This eliminates the need for server-side spatial queries or custom routing logic.
The subscription system leverages Geohash.neighbors(of:) to optionally include adjacent cells, ensuring users don't miss messages sent from nearby locations that might fall into neighboring geohash boundaries.
Implementation Details
Encoding and Neighbor Calculation
The Geohash.swift file implements lightweight geohash mathematics:
// Encode coordinates at city precision
let geohash = Geohash.encode(
latitude: 37.7749,
longitude: -122.4194,
precision: GeohashChannelLevel.city.precision
)
// Find adjacent cells
let neighbors = Geohash.neighbors(of: geohash)
// Returns ["u4prv", "u4prz", "u4prp", "u4prr", ...]
The encode function converts latitude/longitude into base-32 strings, while neighbors calculates the eight adjacent cells (north, northeast, east, etc.), critical for ensuring continuous coverage near cell boundaries.
Presence Tracking
GeohashPresenceService manages location broadcasting by periodically invoking createGeohashPresenceEvent. This service:
- Monitors device location updates via Core Location
- Calculates the current geohash at the selected precision level
- Emits kind-20001 events every 30 seconds to indicate active presence
- Aggregates incoming presence events from other users to populate the "nearby" interface
These heartbeats contain no message content, minimizing bandwidth while enabling real-time awareness of local participant counts.
Code Example: Publishing to a Location Channel
The following Swift example demonstrates the complete workflow from coordinates to published event:
import Foundation
// Resolve current position
guard let location = try? LocationManager.shared.currentLocation() else {
return
}
// Generate geohash at neighborhood level (6 chars)
let neighborhoodHash = Geohash.encode(
latitude: location.latitude,
longitude: location.longitude,
precision: GeohashChannelLevel.neighborhood.precision
)
// Construct the channel identifier
let channel = GeohashChannel(level: .neighborhood, geohash: neighborhoodHash)
let channelID = ChannelID.location(channel)
// Create ephemeral message (kind 20000)
let messageEvent = try NostrProtocol.createEphemeralGeohashEvent(
content: "Anyone up for coffee at the corner shop?",
geohash: neighborhoodHash,
senderIdentity: myIdentity,
tags: [["nickname", "local_user"]]
)
// Broadcast via Nostr client
nostrClient.publish(event: messageEvent)
// Maintain presence heartbeat
Task {
while isActive {
try await NostrProtocol.createGeohashPresenceEvent(
geohash: neighborhoodHash,
senderIdentity: myIdentity
)
try await Task.sleep(nanoseconds: 30_000_000_000)
}
}
Summary
- Geohash-based location channels scope Nostr subscriptions to specific geographic cells using base-32 encoded coordinates.
- Six hierarchical levels (
buildingthroughregion) map to geohash precisions from 8 to 2 characters, balancing granularity with privacy. - The
GeohashChannelstruct andChannelID.location()wrapper provide type-safe channel identification throughout the BitChat codebase. - Three specialized event types—ephemeral messages, presence heartbeats, and text notes—use the standard
#gtag for relay filtering. Geohash.swifthandles encoding and neighbor calculations, whileGeohashPresenceServicemanages the 30-second heartbeat broadcasts that power the "nearby users" feature.
Frequently Asked Questions
How does BitChat ensure privacy when using geohash-based location channels?
BitChat preserves privacy by sharing only the geohash string, not raw GPS coordinates. Users select precision levels appropriate to their privacy needs—using region (2 characters) reveals only a large geographic area thousands of kilometers wide, while building (8 characters) exposes specific addresses. The geohash acts as a one-way function; observers cannot reverse-engineer exact coordinates without additional data.
What is the difference between a location channel and the global mesh?
A location channel (ChannelID.location) restricts message delivery to subscribers matching a specific geohash tag, creating spatially isolated conversations. The global mesh (ChannelID.mesh) operates without geographic filtering, broadcasting to all connected relays worldwide. Location channels optimize for local latency and relevance, while the mesh serves as the default public timeline for broad announcements.
Why does BitChat use kind-20001 events for presence tracking?
Kind-20001 events function as lightweight heartbeats that announce user presence without carrying message content. This separation of concerns allows the GeohashPresenceService to broadcast every 30 seconds with minimal bandwidth impact, while keeping the actual chat content in separate ephemeral or text note events. Relays can also garbage-collect these presence events more aggressively than permanent text notes.
Can users subscribe to multiple geohash levels simultaneously?
Yes. The LocationStateManager supports concurrent subscriptions to multiple precision levels, and the Geohash.neighbors(of:) function enables subscription to adjacent cells. This prevents message loss near cell boundaries—for instance, a user at u4pru might subscribe to u4prv (east) and u4prz (southeast) to capture messages from users just across the street that fall into neighboring geohash cells.
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 →