Geohash-Based Location Channels in BitChat: Privacy-Preserving Proximity Messaging

Geohash-based location channels in BitChat enable users to communicate with nearby peers by mapping geographic coordinates to discrete, encrypted chat channels without exposing exact GPS positions.

BitChat implements geohash-based location channels as a core mechanism for location-aware, decentralized messaging. The system converts latitude/longitude pairs into compact string identifiers—geohashes—that function as channel addresses on the Nostr protocol. Users join channels corresponding to their physical location at a chosen precision level, enabling regional communication while preserving privacy through deliberate coordinate obfuscation.


How Geohash Channels Work in BitChat

The GeohashChannel Data Model

At the center of BitChat's location system is GeohashChannel, a lightweight value object defined in [bitchat/Nostr/GeoRelayDirectory.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/GeoRelayDirectory.swift). This struct combines two properties:

  • level — A categorical precision indicating geographic scope
  • geohash — The actual geohash string encoding the location

The GeohashChannelLevel enum defines supported precisions in the same file, with each variant specifying a fixed character count (precision) that directly maps to physical area size.

// City-level channel (5-character geohash ≈ 2.4km × 4.8km)
let cityChannel = GeohashChannel(level: .city, geohash: "u4pruy")

// Building-level channel (9-character geohash ≈ 4.8m × 4.8m)
let buildingChannel = GeohashChannel(level: .building, geohash: "u4pruyfku")

Higher precision means smaller physical areas and more specific channels.


Presence Broadcasting Without Privacy Leakage

Ephemeral Nostr Events (Kind 9041)

BitChat uses ephemeral Nostr events to advertise user presence without persistent tracking. Every few seconds, clients broadcast kind 9041 "geohash presence" events containing only the current geohash string—not coordinates, not identity metadata. These events expire quickly, creating a transient footprint.

The formal protocol specification lives in [docs/GeohashPresenceSpec.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/GeohashPresenceSpec.md).

GeohashPresenceService: Heartbeat Management

The [GeohashPresenceService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/GeohashPresenceService.swift) implementation handles three critical functions:

  1. Scheduled broadcasting — Throttles presence events to prevent network spam
  2. Event aggregation — Collects incoming presence beacons from peers
  3. UI publication — Emits consolidated online counts per channel
// Broadcast presence for selected channel
try NostrProtocol.createGeohashPresenceEvent(
    geohash: cityChannel.geohash,
    privateKey: myNostrKey
)

// Subscribe to aggregated presence updates
geohashPresenceService.presencePublisher
    .sink { presence in
        print("🗺️ \(cityChannel.geohash) – online: \(presence.onlineCount)")
    }
    .store(in: &cancellables)

Only the online count reaches the UI—never individual participant lists unless users explicitly message in the channel.


Activity Tracking and Channel Lifecycle

GeohashChatActivityTracker

The [GeohashChatActivityTracker.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/GeohashChatActivityTracker.swift) component monitors engagement patterns across location channels. It records:

  • Message timestamps
  • Reaction activity
  • User join/leave events

This data drives two UX decisions: highlighting "active" locations in the interface and automatically ceasing presence broadcasts for stale channels—reducing unnecessary network traffic and battery consumption.


Geo-Optimized Relay Selection

GeoRelayDirectory

To minimize latency and distribute load, [GeoRelayDirectory.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/GeoRelayDirectory.swift) maps geohash prefixes to geographically proximate Nostr relays. Rather than flooding the global relay mesh with location-specific traffic, BitChat targets relays physically close to the hashed region.

This partitioning strategy ensures:

  • Lower latency for real-time messaging
  • Reduced bandwidth per relay
  • Horizontal scalability as user density increases

Channel Integration Throughout the App

Location channels integrate seamlessly with BitChat's broader channel system via the ChannelID enum. The .location(GeohashChannel) case allows unified handling alongside public, private, and direct message channels.

Test coverage in [bitchatTests/Services/LocationStateManagerTests.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/LocationStateManagerTests.swift) demonstrates complete channel selection flows:

// Select a location channel through the state manager
locationStateManager.select(.location(cityChannel))

UI components including the people list and map view filter all data—messages, presence, reactions—through the currently selected GeohashChannel.


Privacy and Security Guarantees

BitChat's geohash channel design provides multi-layered privacy protection:

Mechanism Protection
Coarse precision levels Exact coordinates never transmitted
Ephemeral presence events No persistent location history on relays
Aggregated counts only Individual identities hidden until explicit messaging
Geohash—not GPS—broadcasts Third parties cannot reverse-engineer precise position

Even at building-level precision, the geohash represents an area, not a point.


Summary

  • GeohashChannel (GeoRelayDirectory.swift) is the core identifier combining precision level and geohash string
  • GeohashPresenceService manages ephemeral kind 9041 events for privacy-preserving online indicators
  • GeohashChatActivityTracker monitors channel engagement to optimize broadcast behavior
  • GeoRelayDirectory routes traffic through geographically optimal relays
  • The .location() channel type integrates with all standard BitChat UI components
  • Privacy emerges from coarse geohashes, ephemeral events, and aggregated presence data

Frequently Asked Questions

What precision levels does BitChat support for geohash channels?

BitChat defines precision through the GeohashChannelLevel enum in GeoRelayDirectory.swift, with variants like .region, .city, and .building. Each maps to a fixed geohash character count—more characters mean smaller physical areas. The exact level definitions depend on the deployed version, but the architecture supports arbitrary precision granularity.

Can other users see my exact location in a geohash channel?

No. The system broadcasts only your geohash string, which encodes a rectangular area, not GPS coordinates. At city-level precision, a single geohash covers several square kilometers. Other users see only an online count unless you actively send messages revealing your participation.

How does BitChat prevent location channel spam?

Three mechanisms work together: ephemeral events (kind 9041) expire automatically rather than persisting; GeohashPresenceService throttles broadcast frequency; and GeohashChatActivityTracker halts presence broadcasts for inactive channels. Additionally, relay selection in GeoRelayDirectory partitions traffic geographically rather than globally.

Are geohash presence events stored on Nostr relays?

No. Per the Geohash Presence Specification, these events use Nostr's ephemeral event handling (kind 9041). They are transmitted to connected clients but not retained by relays, eliminating persistent location trails.

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 →