Implementation of Location-Based Channels Using Geohash Coordinates in BitChat
BitChat implements location-based channels by encoding GPS coordinates into hierarchical Geohash strings, mapping each precision level to a distinct channel while restricting presence broadcasts to low-precision zones to protect user privacy.
BitChat, an open-source decentralized messaging application from the permissionlesstech/bitchat repository, enables proximity-based conversations without centralized infrastructure. The system converts geographic coordinates into compact Geohash identifiers, creating a hierarchy of channels that range from regional scope down to individual buildings while integrating with the Nostr protocol for decentralized message relay.
Core Geohash Channel Architecture
The foundation of BitChat's location system resides in bitchat/Protocols/LocationChannel.swift, which defines the data structures that bridge GPS coordinates to channel identifiers.
GeohashChannelLevel Enum
The GeohashChannelLevel enum establishes a six-tier precision hierarchy:
enum GeohashChannelLevel: CaseIterable, Codable, Equatable {
case building, block, neighborhood, city, province, region
var precision: Int {
// Returns length of geohash string:
// region=2, province=3, city=4, neighborhood=5, block=6, building=8
}
var displayName: String {
// Returns UI-friendly names like "City", "Building", etc.
}
}
Each level corresponds to a specific geohash string length—region uses 2 characters while building uses 8—creating increasingly precise geographic boundaries.
GeohashChannel and ChannelID Types
The GeohashChannel struct bundles a precision level with its computed geohash string:
struct GeohashChannel: Codable, Equatable, Hashable, Identifiable {
let level: GeohashChannelLevel // e.g., .city
let geohash: String // e.g., "u4pru"
var id: String { "\(level)-\(geohash)" }
var displayName: String { "\(level.displayName) • \(geohash)" }
}
The ChannelID enum distinguishes between the global mesh channel and location-based channels:
enum ChannelID: Equatable, Codable {
case mesh
case location(GeohashChannel) // Wraps the geohash channel
}
From GPS Coordinates to Channel IDs
BitChat transforms raw location data into channel identifiers through a four-stage pipeline validated in bitchatTests/LocationChannelsTests.swift:
- Location Sampling – CoreLocation provides latitude and longitude updates.
- Geohash Encoding – The app encodes coordinates to a full-precision 8-character string.
- Level Derivation – The system truncates the string to match each
GeohashChannelLevel.precisionvalue, generating a hierarchy of channel identifiers (region → province → city → neighborhood → block → building). - Channel List Publication –
LocationChannelsModelpublishes an array ofGeohashChannelobjects, one per level, for the UI layer inLocationChannelsSheet.swift.
Bridging to the Nostr Protocol
Each geohash channel maps to a derived public key unique to that geographic string, enabling decentralized routing without central coordination. According to the docs/GeohashPresenceSpec.md specification, BitChat uses geohash-scoped Nostr tags and specialized event kinds:
- Chat Messages – Broadcast with
kind 20000and the tag["g", "<geohash>"] - Presence Heartbeats – Sent as ephemeral
kind 20001events using the same geohash tag
This design allows relays to filter and forward messages based on geographic scope while maintaining the permissionless nature of the Nostr network.
Presence Broadcasting and Privacy Controls
The GeohashPresenceService (tested in bitchatTests/Services/GeohashPresenceServiceTests.swift) manages heartbeat transmissions that signal user availability without exposing precise locations.
Randomized Broadcast Loop
When the app enters the foreground, the service initiates a randomized loop with the following characteristics:
- Interval timing – 40 to 80 seconds between broadcast cycles
- Decorrelation delays – 2 to 5 second randomized delays between level transmissions to prevent timing correlation attacks
Precision-Based Privacy Restrictions
BitChat enforces strict privacy boundaries for presence data:
- Low-precision only – Presence heartbeats broadcast exclusively for region, province, and city levels (precision ≤ 5 characters)
- High-precision silence – Neighborhood, block, and building channels never transmit presence data
- Ambiguous indicators – The UI displays "
[? people]" for high-precision channels where presence counting is disabled, preventing misleading assumptions about participant visibility
UI Integration and Activity Tracking
The user interface in bitchat/Views/LocationChannelsSheet.swift renders the channel hierarchy using GeohashChannel.levelTitle for headers and GeohashChannel.displayName for subtitles.
Participant Counting
GeohashChatActivityTracker (validated in bitchatTests/Services/GeohashChatActivityTrackerTests.swift) aggregates participant counts by monitoring unique public keys across both chat events (kind 20000) and presence events (kind 20001). For channels where presence is disabled, the tracker relies solely on active chat participants, with the "?" indicator signaling incomplete visibility.
Summary
- Hierarchical precision – Six levels from region (2-character geohash) to building (8-character geohash) provide granular location targeting.
- Nostr integration –
kind 20000handles chat messages whilekind 20001manages ephemeral presence heartbeats, both scoped via["g", "<geohash>"]tags. - Privacy by design – Presence broadcasts limited to low-precision levels (≤5 characters) prevent fine-grained location tracking.
- Decentralized identity – Each geohash maps to a derived public key, enabling permissionless channel participation without central registration.
Frequently Asked Questions
What precision levels does BitChat support for location channels?
BitChat supports six precision levels defined in GeohashChannelLevel: region (2 characters), province (3 characters), city (4 characters), neighborhood (5 characters), block (6 characters), and building (8 characters). Each level truncates the full geohash string to create increasingly specific geographic boundaries.
How does BitChat protect privacy in location-based channels?
BitChat restricts presence heartbeats to low-precision channels (region, province, and city) where the geohash string is 5 characters or shorter. High-precision channels (neighborhood, block, building) never broadcast presence data, and the UI displays "[? people]" to indicate uncertainty about participant counts in these sensitive zones.
What Nostr event kinds does BitChat use for geohash channels?
BitChat utilizes kind 20000 for chat messages and the ephemeral kind 20001 for presence heartbeats. Both event types include the ["g", "<geohash>"] tag to enable geographic scoping on Nostr relays, as documented in docs/GeohashPresenceSpec.md.
How does the presence heartbeat system prevent timing correlation?
The GeohashPresenceService implements randomized timing with two mechanisms: a 40–80 second interval between broadcast cycles and 2–5 second randomized delays between transmissions for different precision levels. This decorrelation prevents observers from linking presence events across different geographic scales to a single user.
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 →