All BLE mesh communication in Bitchat uses a compact binary packet format optimized for Bluetooth LE’s constrained bandwidth and MTU limitations. The format is designed around network byte order (big-endian) multi-byte values, variable-length fields with explicit length prefixes, and an optional Ed25519 signature that covers the full packet except the TTL byte. Two versions of the format exist: V1 (legacy, 2-byte payload length) and V2 (current, 4-byte payload length with optional source route).Documentation Index
Fetch the complete documentation index at: https://mintlify.com/permissionlesstech/bitchat/llms.txt
Use this file to discover all available pages before exploring further.
V1 Packet Structure
V1 is the legacy format. The fixed header is 14 bytes; the payload length field is 2 bytes, limiting payloads to 65 535 bytes before fragmentation.V2 Packet Structure
V2 extends the header to 16 bytes by widening the payload length field to 4 bytes (UInt32). It also adds an optional SOURCE ROUTE section between the recipient ID and the payload, enabled by the HAS_ROUTE (0x08) flag. The source route may only be present on packets that also carry a RecipientID (HAS_RECIPIENT flag set).
RecipientID is required whenever HAS_ROUTE is set.
Fixed Header Fields
| Field | V1 Size | V2 Size | Description |
|---|---|---|---|
| Version | 1 byte | 1 byte | 0x01 (V1) or 0x02 (V2) |
| Type | 1 byte | 1 byte | Packet type identifier (see table below) |
| TTL | 1 byte | 1 byte | Remaining relay hops; decremented by each relay. Excluded from signature scope so relays can decrement without invalidating it |
| Timestamp | 8 bytes | 8 bytes | Milliseconds since Unix epoch (big-endian UInt64) |
| Flags | 1 byte | 1 byte | Bitfield: HAS_SENDER (0x01), HAS_RECIPIENT (0x02), HAS_SIGNATURE (0x04), HAS_ROUTE (0x08) |
| Payload Length | 2 bytes | 4 bytes | Length of the payload in bytes. In V2 this is a UInt32 and excludes the route section, IDs, and signature |
| Total Header Size | 14 bytes | 16 bytes | V2 is 2 bytes larger due to the wider length field |
Packet Types
Bitchat defines the following packet types on the mesh. The first byte of the fixed header identifies the type.Messaging and Encryption
Messaging and Encryption
| Type | Raw Value | Description |
|---|---|---|
noiseEncrypted | 0x11 | A Noise-session-encrypted envelope. All private payloads — private messages, delivery acknowledgments, read receipts, voice frames, private file transfers — ride inside this type. Intermediate relays see only opaque ciphertext. |
noiseHandshake | 0x10 | Noise XX handshake message. Establishes a mutually-authenticated, forward-secret session between two peers. |
message | 0x02 | Signed public broadcast message (plain text, visible to everyone in the mesh). |
groupMessage | 0x25 | Group-encrypted broadcast (cleartext group ID, ChaChaPoly body). Opaque ciphertext to non-members; carried and served like a public message for history sync. |
Presence and Control
Presence and Control
| Type | Raw Value | Description |
|---|---|---|
announce | 0x01 | Signed peer-presence announcement. Carries nickname, Noise static public key, Ed25519 signing key, capability flags, and up to 10 direct-neighbor IDs. Propagates multi-hop. |
leave | 0x03 | Peer departure notification. Signals that a peer is intentionally disconnecting. |
ping / pong | 0x26 / 0x27 | Directed echo request and reply (nonce + origin TTL). Never replayed via gossip sync. |
Fragmentation, Files, and Voice
Fragmentation, Files, and Voice
| Type | Raw Value | Description |
|---|---|---|
fragment | 0x20 | A single fragment of a larger packet that exceeded the link MTU. Contains an 8-byte fragment ID, 2-byte index, 2-byte total, 1-byte original type, and fragment data. Always uses full fanout (never subset). |
fileTransfer | 0x22 | A public file or image transfer over the mesh. Encoded as a BitchatFilePacket TLV payload containing filename, file size, MIME type, and content. Private file transfers ride inside noiseEncrypted. |
voiceFrame | 0x29 | Push-to-talk live voice burst. Carried as a public broadcast; stale audio frames are dropped at the receiver. Never replayed via gossip sync. Private voice bursts ride inside noiseEncrypted. |
Board and Sync
Board and Sync
| Type | Raw Value | Description |
|---|---|---|
boardPost | 0x23 | Signed geohash bulletin board post or tombstone. Long-lived (synced until the post’s own expiry) with a separate gossip sync capacity and cadence. |
requestSync | 0x21 | Gossip sync initiation. Carries a compact GCS filter encoding what the sender already has, so the peer can return only missing packets. TTL is always 0 (local only). |
Store-and-Forward
Store-and-Forward
| Type | Raw Value | Description |
|---|---|---|
courierEnvelope | 0x04 | A sealed (Noise X) store-and-forward envelope addressed by a 16-byte rotating recipient tag. Directed traffic; never relayed via gossip sync. See Store & Forward. |
prekeyBundle | 0x24 | A signed prekey bundle published for offline key agreement. Gossiped like board posts; one per owner, newer replaces older. |
nostrCarrier | 0x28 | Gateway carrier for Nostr bridging. A complete signed Nostr event ferried between a mesh-only peer and an internet gateway peer. Never replayed via gossip sync. |
Noise Payload Types
ThenoiseEncrypted packet type is a container. After decryption, the first byte of the plaintext indicates the inner payload type:
| Inner Type | Raw Value | Description |
|---|---|---|
privateMessage | 0x01 | Private chat message between two peers |
readReceipt | 0x02 | Message was read by the recipient |
delivered | 0x03 | Message was delivered |
groupInvite | 0x06 | Creator-signed group state (invite or initial roster) |
groupKeyUpdate | 0x07 | Creator-signed group state (key rotation or roster update) |
voiceFrame | 0x08 | Live voice burst frame (private push-to-talk) |
privateFile | 0x20 | Private file transfer (BitchatFilePacket encoded inside Noise) |
authenticatedPeerState | 0x21 | Versioned peer capability state, authenticated by the Noise session |
verifyChallenge | 0x10 | QR-based out-of-band verification challenge |
verifyResponse | 0x11 | QR-based out-of-band verification response |
vouch | 0x12 | Batch of transitive-verification vouch attestations |
Padding
OnlynoiseEncrypted and noiseHandshake packets are padded. Every other type — public messages, announcements, board posts, group messages, fragments, files, and voice frames — is transmitted at its natural length, making payload length observable to any passive BLE listener.
Padding uses PKCS#7-style byte filling: each padding byte equals the total pad length. Frames are padded toward the next bucket:
| Payload bucket | Target size |
|---|---|
| Small | 256 bytes |
| Medium | 512 bytes |
| Large | 1 024 bytes |
| Extra-large | 2 048 bytes |
Signatures
Packets carrying an Ed25519 signature include it as the final 64 bytes of the wire frame (whenHAS_SIGNATURE is set in the flags field). The signature is computed over the entire packet structure — fixed header, sender ID, recipient ID, source route (if present), and payload — with the TTL byte temporarily set to zero and the signature bytes removed before signing.
Setting TTL to zero during signing is what allows intermediate relays to decrement the TTL without invalidating the signature: the signature was never over the original TTL value. Tampering with any other field (including the route list in a V2 source-routed packet) invalidates the signature and causes the receiving peer to drop the packet.
Source Routing (V2)
Source routing is a V2-only feature controlled by theHAS_ROUTE (0x08) flag in the flags byte. When set, a SOURCE ROUTE section appears immediately after the RecipientID:
Count byte gives the number of intermediate hops (not counting sender or recipient, which are already in the fixed header). A relay finds its own peer ID at index i in the hop list and forwards to the peer at index i+1; if it is the last hop, it forwards directly to the RecipientID. If the next hop is unreachable, the relay falls back to flooding.
The
HAS_ROUTE flag is only valid when version >= 2. A V1 relay receiving a packet with this flag set must ignore it. iOS only originates source-routed packets when every node on the intended path has been observed speaking V2, ensuring that V1 peers never receive a frame they cannot decode.SOURCE_ROUTING.md design document in the repository.