How BitChat Implements Geographic Chat Rooms (Location Channels)

BitChat implements geographic chat rooms using hierarchical geohash strings derived from GPS coordinates, where LocationStateManager generates precision-scoped channels and GeohashPresenceService broadcasts anonymized presence heartbeats to Nostr relays for low-precision levels only.

BitChat's Location Channels enable users to join chat rooms scoped to their physical location without exposing precise coordinates. According to the permissionlesstech/bitchat source code, the implementation relies on geohash encoding to map latitude and longitude into hierarchical string identifiers, creating a dual-transport model where mesh chats remain local while location-based messages route through the global Nostr network.

Core Architecture Components

GeohashChannelLevel and GeohashChannel

In bitchat/Protocols/LocationChannel.swift, the GeohashChannelLevel enum defines five precision levels ranging from region (2 characters) to building (8 characters). The GeohashChannel struct holds the computed geohash string and its corresponding level, enabling type-safe channel identification throughout the codebase.

LocationStateManager

The LocationStateManager class in bitchat/Services/LocationStateManager.swift serves as the central coordinator. It uses CoreLocation to obtain one-shot GPS fixes, then generates a full set of GeohashChannel instances via Geohash.encode(latitude:longitude:precision:). This produces varying precision strings: length 2 for region, 4 for province, 6 for city, and 8 for building. The manager exposes availableChannels and selectedChannel as published properties for UI observation.

GeohashPresenceService

Located in bitchat/Services/GeohashPresenceService.swift, this service handles decentralized presence broadcasting. Every 40-80 seconds, it generates Kind 20001 Nostr events for low-precision geohash channels (region, province, city) while explicitly excluding neighborhood, block, and building levels to prevent fine-grained location leakage.

How Geographic Chat Rooms Work

  1. Location Acquisition – When users enable location channels, LocationStateManager.enableLocationChannels() requests CoreLocation permissions and triggers a one-shot location request.

  2. Geohash Generation – In LocationStateManager.computeChannels(from:), the system iterates through GeohashChannelLevel.allCases, calling Geohash.encode(latitude:longitude:precision:) for each level to produce strings of appropriate length. The resulting array populates availableChannels.

  3. Channel Selection – UI components like LocationChannelsSheet invoke LocationStateManager.select(_:) with a ChannelID.location(GeohashChannel) parameter. The manager persists the selection and sets the teleported flag when users manually select a geohash outside their current GPS location.

  4. Presence Broadcasting – GeohashPresenceService monitors availableChannels and schedules heartbeat tasks every 40-80 seconds. Each task applies a random 2-5 second delay, derives a temporary Nostr identity via NostrIdentityBridge.deriveIdentity(forGeohash:), constructs a Kind 20001 event using NostrProtocol.createGeohashPresenceEvent, and publishes to relays returned by GeoRelayDirectory.closestRelays(toGeohash:count:).

  5. Privacy Enforcement – The service validates precision through allowedPrecisions.contains(channel.geohash.count), ensuring only region, province, and city-level geohashes (lengths 2, 4, and 6) receive presence broadcasts while higher-precision levels remain local-only.

  6. Reverse Geocoding – LocationStateManager uses CLGeocoder to convert coordinates into human-readable names (e.g., "San Francisco • u4pruy"), storing these in locationNames for UI display and bookmark resolution.

Implementation Code Examples

// Create a location channel for the current city level
let cityChannel = GeohashChannel(level: .city,
                                 geohash: "u4pruy")   // 6-character city-level geohash

// Switch UI to that channel
LocationStateManager.shared.select(.location(cityChannel))

// Manually trigger a presence heartbeat for debugging
await GeohashPresenceService.shared.performHeartbeat()

// Add a persistent geohash bookmark
LocationStateManager.shared.addBookmark("9q8yy")

Key Source Files

Summary

  • BitChat uses geohash encoding to convert GPS coordinates into hierarchical string identifiers representing geographic areas from region to building precision.
  • The LocationStateManager generates multiple precision levels simultaneously, allowing users to select granularity from coarse (regional) to fine (building-level) chat rooms.
  • Privacy protection occurs at the network layer: GeohashPresenceService broadcasts presence only for low-precision geohashes (2-6 characters) to Nostr relays, keeping precise location data local.
  • The dual-transport model routes mesh-only conversations locally while propagating location-based discovery through the global Nostr relay network using Kind 20001 events.
  • Users can teleport to arbitrary geohashes or bookmark specific locations for persistent access without physical presence.

Frequently Asked Questions

What geohash precision levels does BitChat support?

BitChat implements five hierarchical levels defined in GeohashChannelLevel: region (2 characters), province (4 characters), city (6 characters), neighborhood (7 characters), block (7+ characters), and building (8 characters). The UI in LocationChannelsSheet presents these as nested geographic scopes for user selection.

How does BitChat prevent precise location leaks over Nostr?

The GeohashPresenceService explicitly filters broadcasts using allowedPrecisions.contains(channel.geohash.count), restricting presence heartbeats to geohashes of length 2, 4, or 6 (region through city). Higher-precision levels representing neighborhoods, blocks, and buildings never generate Kind 20001 events, ensuring fine-grained location data remains confined to local mesh transmission.

Can users join location channels without sharing their GPS location?

Yes. The teleported flag in LocationStateManager indicates when a user selects a geohash channel not matching their current GPS coordinates. Users can manually enter geohash strings or select from bookmarks using LocationStateManager.shared.addBookmark(), enabling participation in distant geographic chat rooms without physical presence or revealing their actual location.

How often does BitChat broadcast presence heartbeats?

GeohashPresenceService schedules presence broadcasts every 40-80 seconds with a random 2-5 second jitter delay per channel. This decorrelation prevents traffic spikes while maintaining sufficient presence granularity for the Nostr relay network to route messages to active geographic cells.

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 →