# How NostrCarrier Packets Bridge BitChat’s Mesh Network with the Internet

> Discover how NostrCarrier packets seamlessly bridge BitChat offline mesh networks with the internet. Learn how these signed payloads enable global message relay.

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

---

**NostrCarrier packets are signed, TLV-encoded payloads that traverse BitChat’s Bluetooth LE mesh as ordinary traffic, enabling gateway nodes to verify signatures and relay messages between offline mesh devices and public Nostr relays.**

BitChat creates a peer-to-peer mesh network over local Bluetooth LE links, intentionally isolating communication from the internet. To enable mesh-only devices to exchange messages with the global Nostr network without breaking the decentralized model, the app uses **NostrCarrier packets**—a specialized protocol defined in [`bitchat/Protocols/NostrCarrierPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/NostrCarrierPacket.swift). According to the permissionlesstech/bitchat source code, these packets wrap cryptographically signed Nostr events in a transport-agnostic format that mesh peers treat as standard data, while designated gateways interpret them as bridging instructions.

## Packet Structure and TLV Encoding

The `NostrCarrierPacket` struct defines a binary format optimized for constrained mesh networks. It uses Type-Length-Value (TLV) encoding with 2-byte big-endian length fields.

### Core Fields

Each carrier contains three critical components:

- **direction**: An enum value (`toGateway`, `fromGateway`, `toBridge`, `fromBridge`) indicating routing intent—whether the packet is a directed request toward a gateway or a broadcast originating from one.
- **geohash**: A 12-character location identifier serving as the Nostr "room" name, limited to `maxGeohashLength` bytes.
- **eventJSON**: The complete signed Nostr event including `id`, `pubkey`, `created_at`, `kind`, `tags`, `content`, and `sig`. The implementation caps this at `maxEventJSONBytes` (16 KB) to prevent mesh congestion.

The event data remains unencrypted because Nostr events are public by design, but the embedded Schnorr signature ensures authenticity.

### Serialization Logic

The `encode()` method (lines 88–102) constructs the TLV byte stream by writing each field type, length, and value sequentially. Conversely, `decode(_:)` (lines 5–44) iterates over TLV blocks to reconstruct the packet. The decoder is intentionally tolerant—unknown TLV types are skipped (lines 31–33)—ensuring forward compatibility as the protocol evolves.

```swift
// Encoding a packet for mesh broadcast
let packet = NostrCarrierPacket(
    direction: .toGateway,
    geohash: "u4pruy",
    event: signedEvent
)!
let payload = packet.encode()!

```

## Bridging Flow: Mesh to Internet

When a mesh-only device needs to publish to Nostr, it creates a carrier packet with direction `.toGateway` and broadcasts it via BLE.

### Gateway Reception and Verification

Upon receiving the payload, `GatewayService.handleMeshCarrier(_:from:directedTo:)` (lines 164–172) executes a strict validation pipeline:

1. **Structural decoding**: Calls `NostrCarrierPacket.decode(payload)` to extract fields.
2. **Size validation**: Enforces `maxEventJSONBytes` limits to prevent resource exhaustion.
3. **Signature verification**: Invokes `event.isValidSignature()` using Schnorr cryptography—gateways reject any packet with invalid signatures before relaying.

Only after these checks does the gateway call `handleUplinkDeposit` to publish the event to configured Nostr relays, linking the offline geohash-based chat room to the global internet.

```swift
// Inside GatewayService.swift (lines 164-172)
func handleMeshCarrier(_ payload: Data, from peerID: PeerID, directedToUs: Bool) {
    guard let carrier = NostrCarrierPacket.decode(payload),
          let event = carrier.event(),
          event.isValidSignature() else { return }
    
    if carrier.direction == .toGateway {
        publish(event: event, geohash: carrier.geohash) // handleUplinkDeposit
    }
}

```

## Bridging Flow: Internet to Mesh

For inbound traffic, gateways monitor Nostr relays for events matching subscribed geohashes. When a relevant event arrives, the gateway wraps it in a `NostrCarrierPacket` with direction `.fromGateway` and calls `handleDownlinkBroadcast`.

This broadcasts the payload to all mesh peers via `BLEService.broadcastNostrCarrier(_:)`, injecting internet-originated messages back into the local Bluetooth mesh. Mesh nodes receive these as standard BLE packets, decoding them through the same `NostrCarrierPacket.decode` path used for outbound traffic.

## Cross-Island Bridging with BridgeService

When mesh networks form isolated "islands" without direct internet gateways, **bridge nodes** enable inter-island communication. `BridgeService.handleMeshCarrier(_:from:directedTo:)` (lines 516–523) mirrors the gateway logic but uses `.toBridge` and `.fromBridge` directions.

A bridge node forwards events between disconnected mesh segments and the internet, acting as a relay chain. This architecture allows "island-to-island" messaging where one island’s gateway publishes to Nostr, and a distant island’s bridge pulls the event down into its local mesh.

## BLE Transport Implementation

The mesh layer treats Nostr carriers as opaque data blobs. `BLEService` abstracts the transport details through two helper methods:

- `sendNostrCarrier(_:to:)`: Directed transmission for `toGateway` or `toBridge` packets targeting specific peers.
- `broadcastNostrCarrier(_:)`: Floods the mesh with `fromGateway` or `fromBridge` packets.

Both methods set `MessageType.nostrCarrier` (0x28) in the BLE packet header (lines 6115–6138) and trigger the `onNostrCarrierPacket` callback, which routes data to the appropriate service (`GatewayService` or `BridgeService`).

Mesh initiation occurs in [`ChatViewModelBootstrapper.swift`](https://github.com/permissionlesstech/bitchat/blob/main/ChatViewModelBootstrapper.swift) (lines 353–356), where user messages trigger `broadcastNostrCarrier(payload)` after packet encoding.

## Security and Trust Model

NostrCarrier packets enforce security through cryptographic verification rather than transport encryption. Because Nostr events are publicly visible, the carrier transmits the `eventJSON` in plaintext but includes the original `sig` field.

Gateways validate integrity via `structurallyValidEvent` checks (lines 444–465), ensuring:
- The event size complies with mesh limits.
- The Schnorr signature mathematically proves the event originated from the claimed public key.

This prevents mesh spoofing; only the legitimate per-geohash identity can inject events, even though the BLE transport is promiscuous.

## Summary

- **NostrCarrier packets** wrap signed Nostr events in a TLV format defined in [`bitchat/Protocols/NostrCarrierPacket.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/NostrCarrierPacket.swift), adding direction and geohash metadata.
- **Bidirectional bridging** occurs via `GatewayService`: mesh nodes broadcast `.toGateway` packets for uplink, while gateways inject `.fromGateway` packets for downlink.
- **Bridge nodes** extend coverage to isolated mesh islands using `BridgeService` with `.toBridge` and `.fromBridge` directions.
- **Security relies on Schnorr signatures** verified by gateways before relaying, preventing unauthorized injection into the Nostr network.
- **BLE transport** uses `MessageType.nostrCarrier` (0x28) with 16 KB event size limits to ensure mesh stability.

## Frequently Asked Questions

### What is the maximum size of a NostrCarrier packet?

The implementation limits the Nostr event JSON to 16 KB (`maxEventJSONBytes`) and the geohash to 12 bytes (`maxGeohashLength`), keeping the total carrier payload under 16 KB plus TLV overhead. This prevents Bluetooth LE congestion and ensures compatibility with mesh routing.

### Why doesn't the carrier encrypt the Nostr event?

Encryption is unnecessary because Nostr events are public by design. The carrier relies on the embedded Schnorr signature to prove authenticity. Gateways verify `event.isValidSignature()` before relaying, ensuring only valid events enter the public Nostr network, while the content remains readable to all mesh participants consistent with Nostr’s public timeline model.

### How does a mesh-only device find a gateway?

Mesh nodes broadcast `NostrCarrierPacket` with direction `.toGateway` using `BLEService.broadcastNostrCarrier`. Any gateway peer within BLE range receives the broadcast, decodes the packet, and processes the request. There is no discovery protocol—the mesh flooding ensures reachability to any local gateway without central coordination.

### Can malicious nodes inject fake messages into the mesh?

No. While any peer can broadcast BLE packets, gateways and bridges verify the Schnorr signature inside `NostrCarrierPacket.event()` before acting. Invalid signatures result in immediate rejection at `GatewayService.structurallyValidEvent`. This cryptographic binding ensures only the holder of the private key corresponding to the Nostr public key can inject events, preventing spoofing even on an open mesh.