Sync Protocol

Wire format, message types, encryption, and state merge.


Transport

JSON messages over WebSocket. Computer runs the server (default port 8080), phone is the client. After key exchange, all messages are NaCl secretbox-encrypted.

Encryption lifecycle

Phone                              Computer
  │                                  │
  ├──── key_init (publicKey) ───────>│   Plaintext
  │<──── key_exchange (publicKey) ───┤   Plaintext
  │                                  │
  │  Both derive shared secret via nacl.box.before()
  │  All subsequent messages use nacl.secretbox()
  │                                  │
  ├──── handshake (deviceId, secret, lastSync, autoSync?) ──>│  Encrypted
  │                                  │

Each connection uses fresh ephemeral X25519 keypairs. The shared key is never stored — it exists only for the lifetime of the connection.

Message types

Type Direction Purpose
key_init Phone → Computer Phone's ephemeral public key
key_exchange Computer → Phone Computer's ephemeral public key
handshake Phone → Computer Device ID, pairing secret, last sync timestamp, optional autoSync flag
state_sync Both Full state dump (lists, locked lists, scratchpad, categories)
sync_confirm Computer → Phone Confirms sync with mode: merge, desktop-wins, or phone-wins. In merge mode, includes the computed mergedState payload
sync_cancel Computer → Phone Cancels the pending sync. Phone discards the pending computer state it was holding; no data changes on either side
jot_manifest Phone → Computer Summary of jot media (imageIds, fileIds, audioIds, drawings)
jot_meta_request/response Computer → Phone → Computer Request metadata for a single jot
jot_download_request/response Computer → Phone → Computer Full binary download of jot content
jot_refresh_request/response Computer → Phone → Computer Refresh all jot metadata
jot_clear_request/ack Computer → Phone → Computer Clear jot content on phone
file_request/response Computer → Phone → Computer Download a single binary file
heartbeat Both Keep-alive
debug_log Phone → Computer Phone's debug log lines (written to phone-sync.log)

State merge algorithm

Phone sends its PRE-MERGE state via state_sync. Computer receives it, computes a merge preview, and optionally shows a confirmation dialog to the user. Computer then sends either sync_confirm (with a mode) or sync_cancel. Phone performs the actual merge only after receiving sync_confirm.

Confirmation modes:

Mode Behavior
merge Standard LWW merge (default)
desktop-wins Computer state overwrites phone state on conflicts
phone-wins Phone state overwrites computer state on conflicts

Items: Merge by ID. If both sides have the same item, highest updatedAt wins (LWW). New items from either side are added. Missing items are checked against remoteSince — if the remote has been alive since time X and a local item has updatedAt < X, it was deleted remotely.

Categories: Same LWW by updatedAt. Default categories with updatedAt = 0 that don't exist on the remote are treated as "dropped defaults" (the remote replaced them with custom categories).

Scratchpad: Per-category LWW by timestamp. If local has no content and remote does, remote wins.

Locked lists: Always included in the sync — no opt-out.

Clock skew protection: A 500ms grace window prevents items from being incorrectly deleted when device clocks are slightly out of sync.


See also: Sync | Security | Debug Logging | Sync Confirmation & History