How BitChat Integrates with Nostr Relays for Extended Global Messaging
BitChat extends its Bluetooth mesh network to internet scale by wrapping encrypted messages in Nostr events (kind-1059) and delegating all relay communication to the NostrRelayManager, which maintains WebSocket connections, handles offline queuing, and verifies Schnorr signatures without centralized servers.
BitChat is a decentralized messaging application that combines local Bluetooth mesh networking with global internet fallback using the Nostr protocol. When peers move out of Bluetooth range, the app seamlessly switches to Nostr relays to carry end-to-end encrypted private messages, leveraging the open-source relay ecosystem rather than operating proprietary infrastructure. This architecture allows users to communicate globally while maintaining the same cryptographic protections used in local mesh mode, with fine-grained control over relay selection and Tor routing.
Architecture Overview: Bridging Local Mesh to Global Nostr
BitChat’s global messaging layer re-uses the existing Nostr relay infrastructure through a clean abstraction that separates identity management, transport encoding, and connection handling.
The high-level flow works as follows:
- Identity bridging: Each device generates a Nostr identity via
NostrIdentityBridge, which can derive per-geohash identities for location-based channels. - Transport selection: The
ChatViewModeldecides whether a peer is reachable locally (Bluetooth) or requires internet fallback, handing messages toNostrTransportwhen necessary. - Message encoding: Private payloads are wrapped in BitChat-specific private-envelope events using
NostrEmbeddedBitChat, encrypted with XChaCha20-Poly1305, and encoded as Nostrkind-1059events. - Relay management:
NostrTransportdelegates all network I/O to the singletonNostrRelayManager, which maintains a pool of WebSocket connections to configurable relays. - Inbound verification: Each relay runs an off-main-thread pipeline that verifies Schnorr signatures, de-duplicates events, and dispatches verified messages to registered handlers.
Core Components for Nostr Integration
BitChat’s Nostr integration relies on specialized components that handle distinct responsibilities:
NostrIdentityBridge
Located in bitchat/Nostr/NostrIdentityBridge.swift, this class bridges the BitChat keychain to Nostr keypairs. It derives per-geohash identities for location-based channels and exposes the current Nostr identity to transport layers.
NostrTransport
Implemented in bitchat/Services/NostrTransport.swift, this class conforms to the Transport protocol and serves as the internet fallback layer. It resolves recipient npub identifiers to hex public keys, encrypts content using encodePMForNostr, and forwards events to NostrRelayManager. It also manages ACK pacing through the AckPacer class to avoid rate-limit throttling.
NostrRelayManager
The central coordinator in bitchat/Nostr/NostrRelayManager.swift manages WebSocket lifecycles. It maintains two relay lists: builtInRelays (a hard-coded list of DM-capable relays) and customRelays (user-augmented). The manager handles lazy connection establishment via ensureConnections(to:), implements jittered exponential backoff for reconnections, and respects Tor routing settings.
NostrEmbeddedBitChat
This helper in bitchat/Nostr/NostrEmbeddedBitChat.swift provides static methods to format BitChat-specific envelopes. It constructs the kind-1059 private-message events and ACK notifications that carry the encrypted BitChat payloads.
Sending Messages: From BitChat to Nostr Events
When ChatViewModel determines that a peer requires Nostr transport, the message flows through several encryption and encoding stages before reaching the relay network.
First, NostrTransport.sendPrivateMessage (lines 54-66) resolves the recipient’s Nostr public key via the identity bridge. The content is then encrypted and wrapped using NostrEmbeddedBitChat.encodePMForNostr (lines 60-63), which produces a payload encrypted with XChaCha20-Poly1305.
The transport creates a Nostr event using NostrProtocol.createPrivateMessage with kind-1059 and hands it to NostrRelayManager.shared.sendEvent (lines 45-46). If no relay connection is active, the event is queued via enqueuePendingSend (lines 70-78) until a socket becomes available.
Managing Relay Connections and Policies
NostrRelayManager implements sophisticated connection handling to ensure message delivery across unreliable networks.
Relay Selection
The manager distinguishes between builtInRelays (hard-coded defaults enabled only when users have mutual favorites, location permissions, or active location channels) and customRelays (user-defined endpoints). The default policy is applied via applyDefaultRelayPolicy (lines 8-13).
Connection Handling
Connections are opened lazily through ensureConnections(to:) (lines 31-42), which respects Tor bootstrap states and implements automatic reconnection with jittered exponential delays. The connectToRelay flow (lines 108-129) establishes WebSocket sessions and configures the inbound pipeline.
Inbound Verification
Each connected relay spawns an InboundFrameRouter that parses WebSocket frames and verifies Schnorr signatures using event.isValidSignature(). The precheckInboundEvent and deliverVerifiedInboundEvent methods (lines 99-115) ensure only cryptographically valid events reach the UI layer, preventing spam and forgery.
Practical Code Examples
Sending a Private Message via Nostr
let transport = NostrTransport(
keychain: KeychainManager.shared,
idBridge: NostrIdentityBridge()
)
let peerID = PeerID(str: "…") // Short or full Noise ID
let message = "Hello from BitChat!"
let messageID = UUID().uuidString
transport.sendPrivateMessage(
message,
to: peerID,
recipientNickname: "Bob",
messageID: messageID
)
This resolves the recipient’s Nostr pubkey, encrypts the content with XChaCha20-Poly1305, and transmits it via available relays.
Queuing During Offline States
// The app is offline or Tor is still bootstrapping
transport.sendPrivateMessage("Queued msg", to: peerID, recipientNickname: "Bob", messageID: "1234")
When NostrRelayManager has no active connection, the event enters messageQueue. Once any target relay connects, flushMessageQueue automatically transmits pending messages.
Subscribing to Inbound Events
let filter = NostrFilter(kinds: [1059], authors: nil /*…*/)
NostrRelayManager.shared.subscribe(
filter: filter,
id: "bitchat-pm-sub",
handler: { event in
BitChatMessageProcessor.process(event)
},
onEOSE: {
print("Initial history loaded")
}
)
The subscription queues until relays connect, then processes incoming events through the signature-verification pipeline before handing them to the chat UI.
Adding Custom Relays
NostrRelaySettings.addCustomRelay(url: "wss://my-relay.example.com")
This posts a change notification observed by NostrRelayManager (lines 57-67), which reloads the custom list and immediately calls ensureConnections(to:) for the new URLs.
Relay-Aware User Interface
The settings UI in bitchat/Views/AppInfoView.swift (lines 623-695) exposes NostrRelayManager.builtInRelayURLs and NostrRelayManager.relays to display connection health. Users can view built-in relay status, add custom endpoints, and monitor transmission errors without editing configuration files.
Summary
- BitChat uses Nostr relays as an internet-scale fallback when Bluetooth mesh connections are unavailable, avoiding centralized server infrastructure.
- Messages are encrypted using XChaCha20-Poly1305 and wrapped in Nostr
kind-1059events viaNostrEmbeddedBitChatbefore transmission. - The
NostrRelayManagersingleton handles all WebSocket lifecycle management, including lazy connections, exponential backoff reconnection, and offline message queuing. - Security is enforced through Schnorr signature verification in the inbound pipeline and strict relay selection policies that respect user privacy settings.
- Users retain control over which relays to use, with support for both built-in defaults and custom URLs, plus optional Tor routing for metadata protection.
Frequently Asked Questions
What type of Nostr events does BitChat use?
BitChat uses kind-1059 events for private messages. These events contain XChaCha20-Poly1305 encrypted payloads created by NostrEmbeddedBitChat.encodePMForNostr, ensuring that relay operators cannot read message content. ACK receipts for delivery and read notifications use the same private-envelope format.
How does BitChat handle messaging when relays are offline?
When no WebSocket connections are available, NostrRelayManager.sendEvent automatically queues events in messageQueue via enqueuePendingSend. The manager monitors connection states, and once any target relay connects (triggering connectedConnection(for:)), flushMessageQueue transmits all pending messages without user intervention.
Can Nostr relays read BitChat message contents?
No. BitChat encrypts all message content locally using XChaCha20-Poly1305 before placing it in the Nostr event's content field. Relays only see encrypted blobs and Schnorr signatures; they cannot decrypt the actual message text, which is only readable by the intended recipient holding the corresponding private key.
How do users configure which Nostr relays BitChat uses?
Users can add custom relays through NostrRelaySettings.addCustomRelay, which persists URLs and notifies NostrRelayManager to establish new connections immediately. The UI in AppInfoView.swift displays both the hard-coded builtInRelays and user-added endpoints, allowing manual deletion of specific relays or reliance solely on custom infrastructure.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →