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:

  1. Location Sampling – CoreLocation provides latitude and longitude updates.
  2. Geohash Encoding – The app encodes coordinates to a full-precision 8-character string.
  3. Level Derivation – The system truncates the string to match each GeohashChannelLevel.precision value, generating a hierarchy of channel identifiers (region → province → city → neighborhood → block → building).
  4. Channel List Publication – LocationChannelsModel publishes an array of GeohashChannel objects, one per level, for the UI layer in LocationChannelsSheet.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 20000 and the tag ["g", "<geohash>"]
  • Presence Heartbeats – Sent as ephemeral kind 20001 events 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 20000 handles chat messages while kind 20001 manages 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →