How Geohash‑Based Location Channels Work in BitChat: A Technical Deep Dive

BitChat uses hierarchical geohash strings mapped to ephemeral Nostr events to enable location‑aware messaging without exposing exact GPS coordinates.

Geohash‑based location channels are the foundation of BitChat's decentralized, privacy‑preserving proximity chat. The permissionlesstech/bitchat repository implements a multi‑layered system that translates physical coordinates into discoverable communication zones, balances load across geographically distributed relays, and hides precise user locations behind time‑limited presence beacons. This article walks through the Swift implementation, from the core data model to the service layer that keeps the UI updated.


Core Data Model: GeohashChannel and Precision Levels

The architecture centers on GeohashChannel, a value type defined in [bitchat/Nostr/GeoRelayDirectory.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/GeoRelayDirectory.swift). This lightweight struct pairs a geohash string with a level enum that controls spatial granularity.

GeohashChannelLevel Enum

// Defined in GeoRelayDirectory.swift
enum GeohashChannelLevel: Int, CaseIterable {
    case region   // ~5000 km
    case city     // ~2.4 km
    case building // ~0.074 km
    // Each level maps to a specific character count (precision)
}

Each case carries a fixed precision property—the number of characters in the geohash string. Shorter strings cover larger areas. This design lets users join a city‑level channel (precision 5, ~2.4 km) or zoom into a building‑level channel (precision 9, ~74 meters) without revealing their exact position.

Creating a Channel Identifier

// City-level channel: "u4pruy" covers central Paris
let cityChannel = GeohashChannel(level: .city, geohash: "u4pruy")

// Wrapped in ChannelID for use throughout the app
let channelID = ChannelID.location(cityChannel)

The ChannelID.location(_) case integrates geohash channels with BitChat's broader channel system, as demonstrated in [bitchatTests/Services/LocationStateManagerTests.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/LocationStateManagerTests.swift).


Ephemeral Presence Protocol (Kind 9041)

BitChat's privacy guarantees rely on ephemeral Nostr events—messages that expire quickly and leave no permanent record. The [docs/GeohashPresenceSpec.md](https://github.com/permissionlesstech/bitchat/blob/main/docs/GeohashPresenceSpec.md) formalizes this protocol.

Presence Event Structure

  • Kind: 9041 (geohash presence)
  • Content: Empty or minimal metadata
  • Tags: Include the geohash string
  • Expiration: Set to a short window (seconds to minutes)

Clients broadcast these events every few seconds while active in a channel. Because they're ephemeral, relays purge them automatically—preventing long‑term location tracking.

Broadcasting Presence

// Normally triggered automatically by the presence service
try NostrProtocol.createGeohashPresenceEvent(
    geohash: cityChannel.geohash,
    privateKey: myNostrKey
)

The GeohashPresenceService manages this broadcast cycle, including backoff logic when a channel becomes inactive.


Service Layer: Presence, Activity Tracking, and Relay Selection

Three specialized services coordinate to maintain channel state while minimizing network overhead.

GeohashPresenceService

Located in [bitchat/Services/GeohashPresenceService.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/GeohashPresenceService.swift), this service:

  1. Schedules presence broadcasts using a throttled timer
  2. Subscribes to incoming presence events from peers
  3. Aggregates online counts per channel
  4. Publishes consolidated state via presencePublisher
// Observing aggregated presence (online user count)
geohashPresenceService.presencePublisher
    .sink { presence in
        print("🗺️ \(cityChannel.geohash) – online: \(presence.onlineCount)")
    }
    .store(in: &cancellables)

The publisher emits Presence structs containing only the count—not identities—preserving peer anonymity until someone chooses to send an actual message.

Unit tests in [bitchatTests/Services/GeohashPresenceServiceTests.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/GeohashPresenceServiceTests.swift) verify correct scheduling behavior and broadcast throttling under various network conditions.

GeohashChatActivityTracker

Defined in [bitchat/Services/GeohashChatActivityTracker.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/GeohashChatActivityTracker.swift), this service:

  • Records timestamp of last message or reaction per channel
  • Flags channels as "active" or "stale" for UI highlighting
  • Signals GeohashPresenceService to cease broadcasts when a user leaves or a channel goes dormant

This prevents unnecessary network traffic and battery drain from abandoned location sessions.

GeoRelayDirectory: Geographic Relay Sharding

The GeoRelayDirectory maps geohash prefixes to nearby Nostr relays. Instead of broadcasting location events to the entire Nostr network, BitChat:

  1. Hashes the geohash prefix
  2. Selects a deterministic subset of relays within geographic proximity
  3. Establishes subscriptions only to those relays

This relay sharding reduces global bandwidth consumption and improves latency by routing traffic through infrastructure close to the user's physical location.


Integration with BitChat's UI Layer

Location channels flow through the app via ChannelID.location(GeohashChannel):

Component Behavior
LocationStateManager Tracks selected channel; triggers service initialization
People list Filters by current GeohashChannel; shows aggregated presence count
Map view Renders channel boundaries; highlights active geohash cells

The test file [LocationStateManagerTests.swift](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/LocationStateManagerTests.swift) demonstrates this integration: selecting a .location channel ID initializes the presence service, subscribes to appropriate relays, and updates view‑model state.


Privacy and Scalability Design

Privacy Guarantees

  • Coarse coordinates: City‑level geohashes cover square kilometers; building‑level still encompasses tens of meters
  • Ephemeral events: Kind 9041 events expire automatically—no historical location trail
  • Identity separation: Presence counts are anonymous; only message senders reveal their public keys

Scalability Mechanisms

  • Channel partitioning: Each geohash/precision pair is an independent communication zone
  • Relay sharding: Geographic mapping ensures no single relay handles global traffic
  • Broadcast throttling: GeohashPresenceService reduces frequency for inactive channels

Summary

BitChat's geohash‑based location channels combine established geospatial encoding with Nostr's decentralized event model:

Together these components enable proximity‑based messaging that scales without central coordination and protects user privacy by design.


Frequently Asked Questions

What prevents other users from tracking my exact location?

BitChat never transmits raw GPS coordinates. Your location is reduced to a geohash truncated to a chosen precision—for example, a 5‑character city‑level geohash covers approximately 2.4 kilometers. Additionally, presence events use ephemeral Nostr events (kind 9041) that expire automatically, leaving no permanent record of your position.

How does BitChat choose which Nostr relays to use for location channels?

The GeoRelayDirectory performs geographic relay sharding: it maps your geohash prefix to a deterministic subset of relays that are physically close to that region. This reduces latency by routing through nearby infrastructure and prevents any single relay from becoming a global bottleneck for location traffic.

Why does presence broadcast stop after I leave a channel?

The GeohashChatActivityTracker monitors message and reaction timestamps per channel. When a channel shows no activity for a configured timeout period, the tracker signals GeohashPresenceService to stop broadcasting presence and tear down subscriptions, conserving battery and network resources.

Can I join multiple location channels simultaneously?

Currently, BitChat's LocationStateManager maintains one active location channel at a time, selected via select(.location(GeohashChannel)). The architecture supports switching between precision levels (region, city, building) atomically, with each switch reinitializing the presence service and relay connections for the new geohash.

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 →