# How BitChat Implements Location-Based Channels Using Nostr: A Technical Deep Dive

> Discover how BitChat uses Nostr's geohash tags to build decentralized location based chat rooms. Explore the technical implementation of this innovative messaging system.

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

---

**BitChat creates geographic chat rooms by encoding latitude-longitude pairs into geohashes and attaching them to Nostr events via a dedicated `g` tag, enabling decentralized location-based messaging without central servers.**

BitChat is an iOS application that builds peer-to-peer chat functionality on the Nostr protocol. The app implements **location-based channels** by treating geographic coordinates as channel identifiers, allowing users to join chat rooms scoped to specific cities, blocks, or custom regions. According to the `permissionlesstech/bitchat` source code, this implementation relies on a combination of geohash encoding, specialized Nostr tags, and geographic relay discovery.

## Understanding the Geohash Channel Architecture

At the core of BitChat's location-based system lies a hierarchical model that converts physical coordinates into string identifiers suitable for the Nostr protocol.

### Channel Representation with GeohashChannel

The application models geographic channels using the `GeohashChannel` struct defined in [[`bitchat/Protocols/LocationChannel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/LocationChannel.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/LocationChannel.swift). This struct combines a precision level (city, block, or custom) with a geohash string that encodes the latitude and longitude.

```swift
// Build a city-level location channel
let channel = GeohashChannel(level: .city, geohash: "9q8yy")
let channelID = ChannelID.location(channel)

```

The `ChannelID.location` wrapper encapsulates the geohash channel and provides a type-safe way to pass geographic scope throughout the application layer. The UI renders these channels by combining the human-readable level (e.g., "City") with the truncated geohash, creating identifiable room names like "City: 9q8yy".

### Hierarchical Precision Levels

BitChat supports multiple granularity levels through the `GeohashChannelLevel` enum. Each level corresponds to a different geohash precision, allowing users to chat in broad metropolitan areas or specific street blocks. This hierarchy enables the **location-based channels** to scale from dense urban environments to sparse rural coverage without modifying the underlying Nostr event structure.

## Encoding Geographic Scope with Nostr Tags

Once a channel is defined, BitChat must communicate its geographic boundaries to Nostr relays and other clients. The protocol achieves this through a standardized tagging convention.

### The g Tag Convention

When publishing messages to a location-based channel, BitChat injects a Nostr tag with the identifier `g` into the event's tags array. The tag value contains the raw geohash string that defines the channel's boundaries.

```swift
var event = NostrEvent(
    kind: 1059,                     // BitChat private-envelope kind
    content: encryptedPayload,
    tags: [["g", channel.geohash]] // Geographic scope tag
)

```

This `g` tag acts as a filter key for relays and clients. By convention established in the BitChat protocol, any Nostr event carrying a `g` tag is considered part of a location-based channel rather than a global or user-to-user conversation. The [[`NostrRelayManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrRelayManager.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrRelayManager.swift) file handles the injection of this tag during the publishing phase.

### Tag Extraction and Validation

On the receiving side, BitChat validates incoming events by extracting and matching the geohash tag. The test suite in [[`GeohashPresenceServiceTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/GeohashPresenceServiceTests.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/GeohashPresenceServiceTests.swift) demonstrates this extraction logic, parsing the `g` tag from the raw Nostr event to determine which geographic channel should display the message.

## Relay Discovery and Geographic Routing

BitChat does not assume that all Nostr relays host all location-based channels. Instead, it implements a directory service that maps geohashes to specific relay URLs, optimizing message propagation for geographic locality.

### Mapping Geohashes to Relay Sets

The `GeoRelayDirectory` class, implemented in [[`GeoRelayDirectory.swift`](https://github.com/permissionlesstech/bitchat/blob/main/GeoRelayDirectory.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/GeoRelayDirectory.swift), maintains a mapping between geohash prefixes and sets of Nostr relay URLs. When a user selects a location-based channel, the application queries this directory to determine which relays are responsible for that geographic area.

```swift
let relays = GeoRelayDirectory.shared.relays(for: channel.geohash)

```

This design allows BitChat to shard chat traffic across multiple relays based on geographic regions, reducing global broadcast overhead and improving latency for local conversations.

### Publishing to Location-Specific Relays

The `NostrRelayManager` coordinates the actual message transmission. After resolving the target relays through `GeoRelayDirectory`, the manager publishes the signed Nostr event only to the relevant geographic relay set. This targeted publication ensures that location-based channel messages do not flood unrelated relay infrastructure or appear in users' feeds for different regions.

```swift
await NostrRelayManager.shared.publish(event, to: relays)

```

The `GatewayService` provides a higher-level abstraction that handles envelope encryption before passing the payload to the relay manager, as demonstrated in [[`GatewayServiceTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/GatewayServiceTests.swift)](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/Services/GatewayServiceTests.swift).

## Subscribing and Rendering Location-Scoped Messages

Client-side filtering ensures that users only see messages relevant to their currently selected geographic scope. When BitChat receives a Nostr event from a subscribed relay, it inspects the `g` tag and compares the value against the active `ChannelID.location`.

```swift
func handle(event: NostrEvent) {
    guard let incomingGeohash = event.tags.first(where: { $0.first == "g" })?[1] else {
        return // Event lacks geographic scope
    }
    if incomingGeohash == currentChannel.geohash {
        displayMessage(event) // Render in UI
    }
}

```

This client-side validation acts as a final safety layer, ensuring that even if a relay returns out-of-scope events, the UI filters them before display. The binding between the data model (`GeohashChannel`) and the view layer occurs through the `ChannelID.location` wrapper, maintaining a clean separation between Nostr protocol details and user interface code.

## Summary

- **Geohash Encoding**: BitChat converts geographic coordinates into `GeohashChannel` objects with precision levels, wrapped in `ChannelID.location` for type safety.
- **Nostr Tagging**: Location-based channels use a dedicated `g` tag containing the geohash string to scope events geographically on the Nostr protocol.
- **Relay Directory**: `GeoRelayDirectory` maps geohashes to specific relay URL sets, enabling geographic sharding of message traffic.
- **Selective Publishing**: `NostrRelayManager` publishes channel messages only to relays responsible for the relevant geographic area, optimizing network efficiency.
- **Client Filtering**: Incoming events are validated against the current `ChannelID.location` by extracting the `g` tag, ensuring users only see messages from their selected location-based channels.

## Frequently Asked Questions

### How does BitChat determine which Nostr relays to use for a specific location?

BitChat consults the `GeoRelayDirectory` service, which maintains a mapping between geohash strings and arrays of relay URLs. When a user joins a location-based channel, the application calls `relays(for: channel.geohash)` to resolve the specific set of relays that host that geographic region, then subscribes and publishes exclusively to that set.

### What is the purpose of the `g` tag in BitChat's Nostr implementation?

The `g` tag encodes the geographic scope of a message. Its value contains the geohash string (e.g., `["g", "9q8yy"]`) that identifies which location-based channel the event belongs to. Relays and clients use this tag to route and filter messages without parsing encrypted content, enabling efficient geographic distribution on the Nostr protocol.

### Can users create location-based channels at different precision levels?

Yes. The `GeohashChannelLevel` enum in [`LocationChannel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/LocationChannel.swift) supports multiple granularity levels including city and block precision. Users select their desired precision when creating a channel, which determines the length and specificity of the geohash string used in the `g` tag and relay discovery process.

### How does BitChat prevent users from spoofing their location to access restricted channels?

BitChat relies on client-side verification of the `g` tag and trust-minimized relay assignment through `GeoRelayDirectory`. While the protocol itself does not cryptographically prove physical location (as GPS coordinates are self-reported), the relay directory can implement proof-of-location mechanisms or centralized verification to restrict which geohashes specific relays will accept, creating软性 boundaries for location-based channels.