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

> Discover how BitChat implements geohash-based location channels for private messaging. Learn about hierarchical geohash strings and ephemeral Nostr events in this technical deep dive.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-05

---

**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)](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

```swift
// 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

```swift
// 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)](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)](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

```swift
// 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)](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`

```swift
// 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)](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)](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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/GeoRelayDirectory.swift) 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/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:

- **`GeohashChannel`** and **`GeohashChannelLevel`** define hierarchical, precision‑controlled communication zones in [[`GeoRelayDirectory.swift`](https://github.com/permissionlesstech/bitchat/blob/main/GeoRelayDirectory.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/GeoRelayDirectory.swift)
- **Ephemeral kind `9041` events** broadcast presence without persisting location history, as specified in [[`GeohashPresenceSpec.md`](https://github.com/permissionlesstech/bitchat/blob/main/GeohashPresenceSpec.md)](https://github.com/permissionlesstech/bitchat/blob/main/docs/GeohashPresenceSpec.md)
- **`GeohashPresenceService`** manages the broadcast/subscribe lifecycle and aggregates anonymous online counts
- **`GeohashChatActivityTracker`** optimizes resource usage by detecting stale channels
- **`GeoRelayDirectory`** shards traffic across geographically proximal relays

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.