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

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, the GeohashChannelLevel enum defines six distinct precision tiers. Each case corresponds to a specific geohash character count, where longer strings represent smaller geographic areas:

// 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 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:

// 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, it provides stable identifiers for UI components and network operations:

// 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.

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:

// 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, 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 and 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:

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 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
  • Geohash.encode() generates base-32 strings of specified lengths based on GPS coordinates in 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 view presents all available GeohashChannelLevel cases using their displayName properties (mapped via 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.

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 →