# Geohash-Based Location Channels in BitChat's Nostr Integration: A Complete Technical Guide

> Explore geohash-based location channels in BitChat's Nostr integration. Learn how this technical guide enables hyper-local messaging via compact geohash strings, bypassing centralized servers.

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

---

**Geohash-based location channels in BitChat use compact base-32 geohash strings to scope Nostr relay subscriptions to specific geographic cells, enabling hyper-local messaging without centralized servers.**

BitChat implements a novel location-aware messaging layer on top of the Nostr protocol by encoding latitude and longitude into hierarchical geohash cells. According to the [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat) source code, these channels allow users to broadcast and subscribe to conversations that are automatically restricted to specific geographic areas, from individual buildings to entire regions.

## What Are Geohash-Based Location Channels?

A **geohash-based location channel** is a Nostr relay subscription scoped to a specific geohash cell—a deterministic, compact base-32 string representing a rectangular geographic area. Instead of broadcasting messages globally, BitChat users publish events tagged with their current geohash, allowing relays to filter and deliver only relevant, nearby content.

The system uses the standard Nostr `#g` tag to encode location data, enabling any compatible relay to support spatial filtering without proprietary extensions. This design creates privacy-preserving, local-only chat rooms where membership is determined purely by physical presence within a geohash boundary.

## The Geohash Channel Hierarchy

BitChat organizes location into six distinct administrative levels, each mapping to a specific geohash precision (string length).

### Channel Levels and Precision

In [`LocationChannel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/LocationChannel.swift), the `GeohashChannelLevel` enum defines the hierarchy:

| Level | Precision | Geohash Length | Coverage Area |
|-------|-----------|----------------|---------------|
| **building** | 8 | 8 characters | Specific building |
| **block** | 7 | 7 characters | City block |
| **neighborhood** | 6 | 6 characters | Neighborhood |
| **city** | 5 | 5 characters | City |
| **province** | 3 | 3 characters | Province/State |
| **region** | 2 | 2 characters | Large region |

Higher precision values (8) yield smaller cells for intimate conversations, while lower values (2) create broader channels for regional announcements.

### The GeohashChannel Struct

Each channel is represented by the `GeohashChannel` struct, which pairs a `GeohashChannelLevel` with the actual geohash string. The application exposes these through `ChannelID.location(GeohashChannel)`, distinguishing them from the global `.mesh` channel used for worldwide broadcasts.

```swift
let cityChannel = GeohashChannel(level: .city, geohash: "u4pru")
let channelID = ChannelID.location(cityChannel)

```

`LocationStateManager` tracks the user's selected channel and coordinates UI updates when the device moves between cells.

## Nostr Protocol Integration

BitChat extends standard Nostr events with geospatial metadata while maintaining full protocol compatibility.

### Event Types and Tagging

The [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) file provides three primary event constructors for geohash-based communication:

**`createEphemeralGeohashEvent`** generates kind-20000 events for transient public messages. These include the `["g", geohash]` tag and optionally support nicknames or additional metadata tags.

**`createGeohashPresenceEvent`** creates kind-20001 heartbeat events with empty content. These announce a user's presence in a specific cell without carrying conversation data, enabling the "people nearby" feature.

**`createGeohashTextNote`** produces standard NIP-1 text notes (kind-1) tagged with a geohash, optionally including expiration timestamps for temporary announcements.

All three methods utilize the `#g` tag for standard geohash cells or `#r` tags for bridge-mesh cells, allowing relays to filter subscriptions efficiently.

### Subscription and Routing

When subscribing to a channel, the BitChat client sends filter requests containing the target geohash tag. Because Nostr relays natively support tag-based filtering, the network delivers only events matching the user's current geographic cell or explicitly specified neighbors. This eliminates the need for server-side spatial queries or custom routing logic.

The subscription system leverages `Geohash.neighbors(of:)` to optionally include adjacent cells, ensuring users don't miss messages sent from nearby locations that might fall into neighboring geohash boundaries.

## Implementation Details

### Encoding and Neighbor Calculation

The [`Geohash.swift`](https://github.com/permissionlesstech/bitchat/blob/main/Geohash.swift) file implements lightweight geohash mathematics:

```swift
// Encode coordinates at city precision
let geohash = Geohash.encode(
    latitude: 37.7749,
    longitude: -122.4194,
    precision: GeohashChannelLevel.city.precision
)

// Find adjacent cells
let neighbors = Geohash.neighbors(of: geohash)
// Returns ["u4prv", "u4prz", "u4prp", "u4prr", ...]

```

The `encode` function converts latitude/longitude into base-32 strings, while `neighbors` calculates the eight adjacent cells (north, northeast, east, etc.), critical for ensuring continuous coverage near cell boundaries.

### Presence Tracking

`GeohashPresenceService` manages location broadcasting by periodically invoking `createGeohashPresenceEvent`. This service:

1. Monitors device location updates via Core Location
2. Calculates the current geohash at the selected precision level
3. Emits kind-20001 events every 30 seconds to indicate active presence
4. Aggregates incoming presence events from other users to populate the "nearby" interface

These heartbeats contain no message content, minimizing bandwidth while enabling real-time awareness of local participant counts.

## Code Example: Publishing to a Location Channel

The following Swift example demonstrates the complete workflow from coordinates to published event:

```swift
import Foundation

// Resolve current position
guard let location = try? LocationManager.shared.currentLocation() else {
    return
}

// Generate geohash at neighborhood level (6 chars)
let neighborhoodHash = Geohash.encode(
    latitude: location.latitude,
    longitude: location.longitude,
    precision: GeohashChannelLevel.neighborhood.precision
)

// Construct the channel identifier
let channel = GeohashChannel(level: .neighborhood, geohash: neighborhoodHash)
let channelID = ChannelID.location(channel)

// Create ephemeral message (kind 20000)
let messageEvent = try NostrProtocol.createEphemeralGeohashEvent(
    content: "Anyone up for coffee at the corner shop?",
    geohash: neighborhoodHash,
    senderIdentity: myIdentity,
    tags: [["nickname", "local_user"]]
)

// Broadcast via Nostr client
nostrClient.publish(event: messageEvent)

// Maintain presence heartbeat
Task {
    while isActive {
        try await NostrProtocol.createGeohashPresenceEvent(
            geohash: neighborhoodHash,
            senderIdentity: myIdentity
        )
        try await Task.sleep(nanoseconds: 30_000_000_000)
    }
}

```

## Summary

- **Geohash-based location channels** scope Nostr subscriptions to specific geographic cells using base-32 encoded coordinates.
- Six hierarchical levels (`building` through `region`) map to geohash precisions from 8 to 2 characters, balancing granularity with privacy.
- The `GeohashChannel` struct and `ChannelID.location()` wrapper provide type-safe channel identification throughout the BitChat codebase.
- Three specialized event types—ephemeral messages, presence heartbeats, and text notes—use the standard `#g` tag for relay filtering.
- [`Geohash.swift`](https://github.com/permissionlesstech/bitchat/blob/main/Geohash.swift) handles encoding and neighbor calculations, while `GeohashPresenceService` manages the 30-second heartbeat broadcasts that power the "nearby users" feature.

## Frequently Asked Questions

### How does BitChat ensure privacy when using geohash-based location channels?

BitChat preserves privacy by sharing only the geohash string, not raw GPS coordinates. Users select precision levels appropriate to their privacy needs—using `region` (2 characters) reveals only a large geographic area thousands of kilometers wide, while `building` (8 characters) exposes specific addresses. The geohash acts as a one-way function; observers cannot reverse-engineer exact coordinates without additional data.

### What is the difference between a location channel and the global mesh?

A **location channel** (`ChannelID.location`) restricts message delivery to subscribers matching a specific geohash tag, creating spatially isolated conversations. The **global mesh** (`ChannelID.mesh`) operates without geographic filtering, broadcasting to all connected relays worldwide. Location channels optimize for local latency and relevance, while the mesh serves as the default public timeline for broad announcements.

### Why does BitChat use kind-20001 events for presence tracking?

Kind-20001 events function as lightweight heartbeats that announce user presence without carrying message content. This separation of concerns allows the `GeohashPresenceService` to broadcast every 30 seconds with minimal bandwidth impact, while keeping the actual chat content in separate ephemeral or text note events. Relays can also garbage-collect these presence events more aggressively than permanent text notes.

### Can users subscribe to multiple geohash levels simultaneously?

Yes. The `LocationStateManager` supports concurrent subscriptions to multiple precision levels, and the `Geohash.neighbors(of:)` function enables subscription to adjacent cells. This prevents message loss near cell boundaries—for instance, a user at `u4pru` might subscribe to `u4prv` (east) and `u4prz` (southeast) to capture messages from users just across the street that fall into neighboring geohash cells.