# How Bitchat Implements Location-Based Channels Using Geohash Coordinates with Nostr Relays

> Discover how Bitchat uses geohash coordinates and Nostr relays to implement location-based channels. Learn about deterministic channel identifiers and geohash-specific relay sets.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: how-to-guide
- Published: 2026-08-09

---

**Bitchat maps geohash strings to deterministic channel identifiers and routes Nostr events through geohash-specific relay sets using carrier packets that enforce the `#g` tag constraint.**

The open-source Bitchat protocol (permissionlesstech/bitchat) treats geographic areas as first-class communication channels by encoding location into short geohash strings. This design allows the network to partition message traffic across geographically distributed Nostr relays while maintaining hierarchical granularity from city blocks to individual buildings.

## Core Data Model: GeohashChannel and Granularity Levels

### The GeohashChannel Struct

At the heart of the implementation is the `GeohashChannel` struct defined in [`bitchat/Protocols/LocationChannel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/LocationChannel.swift). This lightweight value type conforms to `Codable`, `Equatable`, `Hashable`, and `Identifiable`, making it suitable for SwiftUI state management and network serialization.

```swift
struct GeohashChannel: Codable, Equatable, Hashable, Identifiable {
    let id = UUID()
    let level: GeohashChannelLevel   // .region, .city, .building, …
    let geohash: String               // e.g., "u4pruydq"
}

```

The `geohash` property serves as the canonical channel identifier. Because geohashes are inherently hierarchical—dropping characters from the right expands the geographic area—the same coordinate can generate channels at multiple granularities without duplication.

### Granularity Levels with GeohashChannelLevel

The `GeohashChannelLevel` enum, co-located in [`LocationChannel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/LocationChannel.swift), assigns semantic meaning to geohash precision. Each case knows its character count, allowing the UI to display appropriate zoom levels and the network layer to select correct relay sets.

```swift
enum GeohashChannelLevel {
    case region     // ~2 characters
    case city       // ~4 characters
    case neighborhood // ~6 characters
    case building   // ~8+ characters
}

```

When a user selects a city-level view, Bitchat instantiates `GeohashChannel(level: .city, geohash: "9q8yy")`, truncating the full precision geohash to the appropriate length for that granularity.

## Relay Discovery and Network Topology

### Mapping Geohashes to Relay Endpoints

Bitchat decouples channel identity from physical infrastructure through a relay lookup function. The `GeohashPresenceService` accepts a closure that maps `(geohash: String, level: Int)` to an array of WebSocket URLs. Test implementations in [`GeohashPresenceServiceTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/GeohashPresenceServiceTests.swift) demonstrate the contract:

```swift
let relayLookup: (String, Int) -> [String] = { geohash, _ in
    ["wss://\(geohash).example"]
}

```

In production deployments, this lookup queries DNS SRV records or parses a local CSV cache (`relays/online_relays_gps.csv`). The script [`scripts/validate_georelays.py`](https://github.com/permissionlesstech/bitchat/blob/main/scripts/validate_georelays.py) audits this mapping, ensuring that every geohash prefix resolves to reachable Nostr relays.

### Validating Relay Mappings

The validation script checks for relay responsiveness and geographic consistency. A well-formed deployment requires that relays advertised for a specific geohash actually peer with the broader network for that region, preventing message silos.

## Publishing and Subscribing to Location Channels

### Wrapping Events in NostrCarrierPacket

Outgoing messages are encapsulated in `NostrCarrierPacket` within `GatewayService`. This carrier attaches the destination geohash metadata required for routing:

```swift
let event = NostrEvent(kind: .textNote, content: "Hello neighborhood!", tags: [["g", "9q8yy"]])
let packet = try NostrCarrierPacket(
    direction: .toGateway,
    geohash: "9q8yy",
    event: event
)
try gatewayService.publish(packet)

```

The packet structure ensures that the transport layer knows which relay set to target without parsing the Nostr event JSON.

### Enforcing Geohash Consistency

`GatewayService` validates that the carrier's `geohash` field matches the `#g` tag inside the wrapped Nostr event. If the values diverge, the packet is rejected before transmission. This integrity check, verified in [`GatewayServiceTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/GatewayServiceTests.swift), prevents accidental cross-posting between geographic channels.

### Presence Tracking with GeohashPresenceService

The `GeohashPresenceService` manages subscriptions to the relay set returned by the lookup function. It surfaces active participants through `GeohashPeopleList`, enabling UI features like "nearby users" indicators. The service follows the specification outlined in [`docs/GeohashPresenceSpec.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/GeohashPresenceSpec.md), which defines how presence metadata is exchanged via Nostr `kind: 2` (recommend server) events tagged with the channel geohash.

## End-to-End Message Flow

The complete lifecycle of a location-based message demonstrates how these components integrate:

1. **Channel Selection**: The user selects a city-level channel, instantiating `GeohashChannel(level: .city, geohash: "9q8yy")`.

2. **Relay Resolution**: The lookup function returns `["wss://9q8yy.example"]` based on the CSV or DNS configuration.

3. **Subscription**: `GatewayService` opens a Nostr `REQ` filter to those relays, subscribing to events with the tag `["g", "9q8yy"]`.

4. **Publishing**: When the user sends a message, Bitchat creates a `NostrCarrierPacket` with the geohash field set to "9q8yy" and the event containing the matching `#g` tag.

5. **Routing**: The gateway publishes to the resolved relays. Recipients subscribed to the same geohash receive the event through their `GeohashPresenceService` subscription.

6. **Rendering**: The UI displays the message within the context of the `GeohashChannel`, maintaining geographic isolation from other channels.

**Production Relay Lookup Example**:

```swift
func relays(for geohash: String) -> [String] {
    let csvPath = Bundle.main.path(forResource: "online_relays_gps", ofType: "csv")!
    return CSVParser.parse(csvPath)[geohash] ?? []
}

```

**Subscribing to Updates**:

```swift
gatewayService.subscribe(to: cityChannel.geohash) { event in
    // Append event to the location-specific timeline
}

```

## Summary

- **GeohashChannel** in [`bitchat/Protocols/LocationChannel.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/LocationChannel.swift) provides the core data structure, combining a geohash string with a granularity level.
- **Relay lookup functions** map geohashes to WebSocket endpoints, enabling geographic partitioning of the Nostr network.
- **NostrCarrierPacket** enforces routing metadata consistency between the carrier envelope and the event's `#g` tag.
- **GeohashPresenceService** handles subscription lifecycle and presence tracking according to the specification in [`docs/GeohashPresenceSpec.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/GeohashPresenceSpec.md).
- The validation script [`scripts/validate_georelays.py`](https://github.com/permissionlesstech/bitchat/blob/main/scripts/validate_georelays.py) ensures relay assignments match their advertised geographic coverage.

## Frequently Asked Questions

### What is a geohash and why does Bitchat use it for location channels?

A geohash is a Base32-encoded string representing a latitude/longitude coordinate with variable precision. Bitchat uses geohashes because they provide a compact, hierarchical addressing scheme that naturally supports geographic granularity. Longer strings represent smaller areas, allowing the protocol to define channels at city, neighborhood, or building levels using the same coordinate system.

### How does Bitchat prevent messages from appearing in the wrong geographic channel?

The `GatewayService` validates that the `geohash` field in the `NostrCarrierPacket` matches the `#g` tag within the wrapped Nostr event. If these values differ, the service rejects the publish operation. This ensures that routing metadata remains consistent with the event's declared geographic scope, preventing misrouted messages.

### Can users join multiple granularity levels simultaneously?

Yes. Because each granularity level generates a distinct `GeohashChannel` with a unique identifier (e.g., "9q" for region vs. "9q8yy" for city), users can subscribe to multiple channels concurrently. The `GeohashPresenceService` maintains separate subscription states for each geohash, allowing parallel participation in both broad regional and hyperlocal building-level conversations.

### How are relays assigned to specific geographic areas?

Relay assignment is handled by a pluggable lookup function that maps geohash strings to WebSocket URLs. Deployments typically use a CSV file (`relays/online_relays_gps.csv`) or DNS SRV records to define these mappings. The [`scripts/validate_georelays.py`](https://github.com/permissionlesstech/bitchat/blob/main/scripts/validate_georelays.py) tool audits this configuration, verifying that advertised relays are online and correctly mapped to their respective geohash prefixes.