# How BitChat Implements Geohash-Based Location Channels for Precision-Scoped Messaging

> Discover how BitChat uses geohash-based location channels with the Nostr protocol to create precision-scoped messaging. Explore chat rooms from city-wide to building-specific scopes.

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

---

**BitChat implements geohash-based location channels by mapping geographic precision levels to specific geohash string lengths, enabling hierarchical chat rooms that range from 2-character regions to 8-character buildings through the Nostr protocol.**

The open-source messaging application BitChat (permissionlesstech/bitchat) uses geohash-based location channels to create geographically scoped chat rooms on the Nostr network. By truncating base-32 geohash strings to specific lengths defined in the `GeohashChannelLevel` enum, the system establishes distinct broadcast domains—from broad regional channels to precise building-level rooms—that operate as independent communication cells.

## Precision Level Architecture

The implementation centers on a strict mapping between human-readable geographic scopes and geohash string lengths. This hierarchy allows users to select communication granularity ranging from regional broadcasts to hyperlocal building chats.

### Enumerating Precision Levels

In [`bitchat/Protocols/LocationChannel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/LocationChannel.swift), the `GeohashChannelLevel` enum defines six distinct precision tiers. Each case corresponds to a specific geohash character count, where longer strings represent smaller geographic areas:

```swift
// LocationChannel.swift
enum GeohashChannelLevel: CaseIterable, Codable, Equatable {
    case building, block, neighborhood, city, province, region
    
    var precision: Int {
        switch self {
        case .building:      return 8
        case .block:         return 7
        case .neighborhood:  return 6
        case .city:          return 5
        case .province:      return 4
        case .region:        return 2
        }
    }
}

```

The **precision** property determines the exact length of the geohash string used for channel identification. An 8-character geohash identifies a specific building, while a 2-character geohash covers an entire region.

### Base-32 Coordinate Encoding

The `Geohash` struct in [`bitchat/Protocols/Geohash.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/Geohash.swift) provides the encoding logic that converts latitude and longitude into base-32 geohash strings. The `encode` method accepts a precision parameter that truncates the output to the desired length:

```swift
// Geohash.swift
static func encode(latitude: Double, longitude: Double, precision: Int) -> String {
    // Base-32 encoding implementation
    // Returns geohash truncated to specified precision
}

```

This function enables dynamic generation of location identifiers at any supported granularity by passing the corresponding `GeohashChannelLevel.precision` value.

## Channel Identification System

BitChat wraps geohash strings in type-safe structures that integrate directly with the Nostr protocol's tagging system.

### GeohashChannel Struct

The `GeohashChannel` struct bundles a precision level with its corresponding geohash string. Defined in [`bitchat/Protocols/LocationChannel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/LocationChannel.swift), it provides stable identifiers for UI components and network operations:

```swift
// LocationChannel.swift
struct GeohashChannel: Codable, Equatable, Hashable, Identifiable {
    let level: GeohashChannelLevel
    let geohash: String  // Already truncated to level.precision
    
    var id: String { "\(level)-\(geohash)" }
    var displayName: String { "\(level.displayName) • \(geohash)" }
}

```

The **id** property creates unique channel identifiers by combining the level name with the geohash, while **displayName** generates human-readable labels used in [`LocationChannelsSheet.swift`](https://github.com/permissionlesstech/bitchat/blob/main/LocationChannelsSheet.swift).

### Nostr Tag Integration

The `ChannelID` enum distinguishes between mesh networking and location-based channels, exposing a computed property that generates the required Nostr "g" tag:

```swift
// LocationChannel.swift
enum ChannelID: Equatable, Codable {
    case mesh
    case location(GeohashChannel)
    
    var nostrGeohashTag: String? {
        switch self {
        case .mesh: return nil
        case .location(let channel): return channel.geohash
        }
    }
}

```

When publishing messages, BitChat extracts the geohash via `nostrGeohashTag` and includes it as a `["g", <geohash>]` tag in Nostr events. This ensures that only subscribers monitoring the specific geohash cell receive the messages.

## Runtime Workflow

The location channel system operates through coordinated interaction between the UI layer and the location service. In [`bitchat/Services/LocationStateManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/LocationStateManager.swift), the application captures the device's coordinates and computes geohashes for the user-selected precision level.

When a user selects a channel level in the UI (managed by [`LocationChannelsSheet.swift`](https://github.com/permissionlesstech/bitchat/blob/main/LocationChannelsSheet.swift) and [`LocationChannelsModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/LocationChannelsModel.swift)), the following sequence occurs:

1. The app retrieves the current latitude and longitude
2. It calls `Geohash.encode(latitude:longitude:precision:)` using the selected `GeohashChannelLevel.precision`
3. It constructs a `GeohashChannel` instance with the resulting string
4. It sets the active `ChannelID.location(channel)` 
5. All subsequent Nostr events include the geohash in the `g` tag, scoping messages to that geographic cell

This workflow enables real-time switching between precision levels without requiring separate coordinate calculations for each tier.

## Practical Implementation Example

The following Swift code demonstrates the complete workflow for creating a city-level channel:

```swift
import BitChat

// Select precision level (5-character geohash)
let level = GeohashChannelLevel.city

// Encode coordinates for San Francisco
let latitude = 37.7749
let longitude = -122.4194
let geohash = Geohash.encode(
    latitude: latitude,
    longitude: longitude,
    precision: level.precision
) // Returns "9q8yy"

// Construct the channel
let cityChannel = GeohashChannel(level: level, geohash: geohash)

// Activate for messaging
let activeChannel = ChannelID.location(cityChannel)

// Extract Nostr tag for event publishing
if let tag = activeChannel.nostrGeohashTag {
    // Publish event with tags: [["g", tag]]
    // Only subscribers to geohash "9q8yy" receive this message
}

```

The [`LocationChannelsModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/LocationChannelsModel.swift) file provides the mapping between `GeohashChannelLevel` and localized display names, enabling the UI to present "City • 9q8yy" rather than raw enum cases.

## Summary

- **GeohashChannelLevel** defines six precision tiers from region (2 characters) to building (8 characters) in [`bitchat/Protocols/LocationChannel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/LocationChannel.swift)
- **Geohash.encode()** generates base-32 strings of specified lengths based on GPS coordinates in [`bitchat/Protocols/Geohash.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/Geohash.swift)
- **GeohashChannel** bundles precision metadata with geohash strings to create unique channel identifiers
- **ChannelID.location** exposes the geohash via `nostrGeohashTag` for inclusion in Nostr "g" tags, ensuring geographic message scoping
- **LocationStateManager.swift** orchestrates the coordinate-to-channel pipeline at runtime

## Frequently Asked Questions

### What geohash precision levels does BitChat support?

BitChat supports six precision levels defined in `GeohashChannelLevel`: region (2 characters), province (4), city (5), neighborhood (6), block (7), and building (8). Each level corresponds to a specific geohash string length that determines the geographic area covered by the channel.

### How does BitChat publish location-scoped messages to Nostr?

When publishing, BitChat retrieves the geohash string from `ChannelID.location.nostrGeohashTag` and includes it as a `g` tag in the Nostr event (format: `["g", <geohash>]`). Relays and clients filter messages based on these tags, ensuring delivery only to subscribers monitoring that specific geohash cell.

### What is the difference between ChannelID.mesh and ChannelID.location?

`ChannelID.mesh` represents the default peer-to-peer messaging mode without geographic scoping, returning `nil` for `nostrGeohashTag`. `ChannelID.location` wraps a `GeohashChannel` struct and provides a geohash string that scopes all messages to a specific geographic area defined by the precision level.

### How does the UI handle different precision levels?

The [`LocationChannelsSheet.swift`](https://github.com/permissionlesstech/bitchat/blob/main/LocationChannelsSheet.swift) view presents all available `GeohashChannelLevel` cases using their `displayName` properties (mapped via [`LocationChannelsModel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/LocationChannelsModel.swift)). When a user selects a level, the app calculates the geohash using that level's precision and updates the active `ChannelID`, immediately switching the message scope to the new geographic granularity.