Protocol

How devices discover each other, establish trust, and exchange messages.

1. Enrollment

User WebAuthn WASM Crypto IndexedDB label + PIN create credential + PRF PRF(32B) + PIN + salt(32B) PBKDF2 + XOR HKDF -> 5 key types public identity (keys, salt) sign device cert (Ed25519) device cert (CBOR) navigate to contacts screen Private keys never leave WASM memory

Recovery

16-word mnemonic phrase (BIP-39 style) encodes the salt. With the same PRF output + PIN + salt, identical keys are derived. The mnemonic does NOT contain the PRF output -- the passkey authenticator is still required.

2. Pairing (Emoji Link)

Alice (initiator) Nostr Relays Bob (responder) generate session_id (32B) HKDF -> 6 emoji code pairing-offer (kind 24242) { nostr_pk, x25519_pk, ed25519_pk, kem_pk, emoji } display link + emoji open link / enter emoji fetch offer (kind 24242) verify emoji match SHA-256(session_id + emoji) pairing-response + emoji_proof verify proof + save contact verify proof + save contact

3. Message Delivery

Relay path (always available)

Sender seal kind 1059 N Relays wss:// unseal try each contact Recipient P2P path (when both online): WebRTC DataChannel -- hybrid encrypted -- ~50ms latency Signaling via sealed events (kind 25002), auto-fallback to relay

Group message path

Generate random AES-256 session key. Encrypt plaintext once with AES-GCM. For each group member: seal the (groupId + sessionKey + ciphertext) individually. Publish N sealed events (one per member). Each member decrypts their event, extracts session key, decrypts message.

4. Voice / Video Calls

1:1 Call Signaling

Caller Nostr Relays Callee getUserMedia(audio, video) sealed SDP offer (kind 25000) getUserMedia(audio, video) sealed SDP answer (kind 25000) ICE candidates (sealed, via relay) P2P media (DTLS-SRTP) Adaptive bitrate monitoring every 3 seconds

Group Call (Mesh)

5. Identity Merging

One person may use multiple devices, each with different keys. Users can manually merge contacts that represent the same person.

AspectBehavior
Storagemerged_contacts IndexedDB store: { mergedId, displayName, deviceKeys }
Contact listOne entry per merged identity
Chat viewMessages from all device keys, merge-sorted by time
SendingEncrypts and delivers to all device keys independently
Unread countsAggregated across all device keys

6. Relay List

Default relays (hardcoded, used on first connection):

wss://relay.damus.io
wss://nos.lol
wss://relay.snort.social
wss://nostr.wine
wss://relay.nostr.band
... (21+ relays for redundancy)

The relay manager connects to all relays in parallel, publishes to all, and subscribes on all. Messages are deduplicated by event ID.

7. Offline Behavior

8. Nostr Event Lifecycle

Event created:
  -> JSON serialized
  -> ID = SHA-256(serialized [0, pubkey, created_at, kind, tags, content])
  -> Signature = BIP-340 Schnorr sign(private_key, ID)
  -> Published: ["EVENT", signed_event_json]

Event received:
  -> Verify ID matches SHA-256 of serialized fields
  -> Verify BIP-340 signature
  -> Route by kind to appropriate handler
  -> For kind 1059: attempt decryption with known contacts

BINARY WIRE PROTOCOL

All internal messages can be encoded as bitpacked binary for BLE transport (20-244 byte MTU). Header byte: [VV][TTTTTT] — 2-bit version, 6-bit type. Backward compatible: first byte 0x7B ({) = legacy JSON.

Size comparison

MessageJSONBinarySavings
Text "hello"80 B19 B76%
Reaction80 B10 B88%
Chess move55 B3 B95%
Location100 B15 B85%
Ping40 B2 B95%
Sketch (10 pts)150 B30 B80%

Dictionary compression

Optional per-message. Marker 0x01 + 1 byte = word index (128 common English words). Marker 0x02 + 1 byte = emoji index (256 emoji). Raw UTF-8 passes through unchanged. Dictionary frozen per protocol version.

Reply-to-message

Type 0x02: includes 8-byte reference to original message + up to 64 bytes quoted preview. Rendered as a quoted bar above the reply bubble. Tap to scroll to original.

REPLY-TO-MESSAGE

Long-press any message to open the action picker. Alongside reaction emoji, a reply button (↩) enters reply mode. The quoted message preview appears above the input field. Send attaches replyToId and replyToPreview to the message. Rendered as a bordered quote bar above the bubble text.

Protocol version 2. Subject to evolution. All changes will be documented.