Pairing & sync
How spaces, invites, discovery, and transport fit together.
UniClipboard splits "how do two devices end up talking to each other?" into three independent layers. Knowing where each one ends helps you reason about failure modes and what's actually on the wire.
| Layer | What it answers | Mechanism |
|---|---|---|
| Trust | Should I accept payloads from this peer? | One-time invite code + space passphrase |
| Discovery | Where is this peer right now? | iroh node IDs + LAN mDNS + rendezvous lookup |
| Transport | How do bytes actually move? | iroh QUIC: direct P2P first, encrypted relay as fallback |
The application-layer payload is encrypted independently of the transport with XChaCha20-Poly1305. The transport could be malicious and the relay could be hostile — they would still see only ciphertext.
Trust: the space + invite model
Devices don't trust each other; they trust a space. A space is the unit of identity, key material, and history. Joining a space is the only way to start syncing.
Creating a space (sponsor)
The first device runs init (CLI) or This is my first device (GUI).
That step:
- Prompts for a passphrase.
- Derives a key-encryption key (KEK) from the passphrase via Argon2id (128 MB memory, 3 iterations, 4 threads).
- Generates a fresh MasterKey (the space's content key) and seals it under the KEK in a local KeySlot file.
- Stores the KEK in the OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service).
There is no "create account" call to any server. The space exists entirely on the sponsor's machine until another device is invited.
Joining a space (joiner)
invite (sponsor) prints a short-lived, one-time invitation code. The
joiner runs join <code> and supplies the same passphrase. Under the
hood:
- The invite code carries the sponsor's iroh node ID and a transient handshake secret. It does not carry the passphrase.
- The two devices run a password-authenticated key exchange — the joiner proves it knows the passphrase without sending it. A wrong passphrase fails the handshake; the sponsor never sees the guess.
- On success, the sponsor wraps the MasterKey under a fresh KEK
derived from the joiner's passphrase entry, and the joiner persists
it the same way the sponsor did during
init. - Both devices add each other to their paired devices list.
After this point, the invite code is consumed. Adding a third device means generating a new invite — pairing is per-join, not per-pair.
Lost a device? Revoke it from any other device in the space (GUI: Devices page; CLI: pending). Subsequent traffic refuses transport from the revoked node ID.
Discovery: finding the other end
Trust tells the daemon which peers to talk to. Discovery tells it where to find them right now. UniClipboard is built on iroh, so each device has a stable node ID (an Ed25519 public key) that survives Wi-Fi changes, sleep, and IP-address churn.
To translate a node ID into a current network address, the daemon runs three lookups in parallel:
- mDNS for LAN discovery. Same broadcast domain → instant resolution. This is what keeps same-Wi-Fi syncs from paying any relay overhead.
- Rendezvous lookup over HTTPS for cross-network discovery. Peers publish signed address records that other peers can fetch by node ID.
- Direct addresses the peer learned about itself from previous sessions.
The first one to return a reachable candidate wins. mDNS keeps the LAN case fast; rendezvous + direct candidates cover everything else.
Transport: how bytes actually move
Once the daemon has a candidate address, iroh's transport stack takes over.
- Direct P2P first. Same network, NAT'd home networks reachable via hole-punching, IPv6 — anywhere a direct path exists, iroh uses it.
- Encrypted relay fallback. If hole-punching fails (symmetric NAT, hostile firewalls, locked-down corporate networks), iroh falls back to an encrypted relay. The relay can route packets between peers but cannot decrypt them.
- QUIC, not raw TCP. Faster connection setup, multiplexed streams, and built-in resilience to network changes (Wi-Fi switches, laptop sleep, mobile-data handoff).
What the relay sees vs. what your peers see
| Observer | Sees |
|---|---|
| The relay | Source / destination node IDs, encrypted packets |
| A network onlooker | QUIC traffic to your peer or to the relay |
| The receiving peer | Decrypted clipboard payload — only paired peers |
The application-layer encryption (XChaCha20-Poly1305 with a 24-byte random nonce) is derived from the space MasterKey, not from the QUIC session keys. Relay compromise does not weaken payload confidentiality.
Mobile companion
Mobile devices use a separate, simpler path than desktop peers. They don't join the iroh trust mesh, they don't get a node ID, and they don't pair via the invite + passphrase + PAKE flow. Instead, the desktop daemon advertises a SyncClipboard-compatible HTTP service on your local network, and a paired mobile client talks to it with HTTP Basic Auth credentials minted at registration.
| Aspect | Desktop ↔ desktop | Desktop ↔ mobile |
|---|---|---|
| Trust unit | Space + passphrase (PAKE) | Per-device username + password (Basic Auth) |
| Identity | iroh node ID (Ed25519 keypair) | Registered device record on the desktop |
| Discovery | mDNS + rendezvous + cached direct addrs | Manual base URL (LAN IP + port) shown at pairing |
| Transport | iroh QUIC, P2P first, encrypted relay fallback | Plain HTTP + Basic Auth — no QUIC, no relay (TLS comes from a reverse proxy) |
| Cross-network | Yes (hole-punching, relay) | Yes, via a server node or Tailscale — not P2P; the phone reaches an HTTPS gateway or overlay address |
| End-to-end key | XChaCha20-Poly1305 under the space MasterKey | TLS-less HTTP; payload hashed and authenticated only |
The phone is always just a SyncClipboard HTTP client — it never becomes an iroh peer. "Cross-network," then, means reaching the gateway from another network rather than P2P: run a headless server node that exposes the gateway as a public HTTPS endpoint, or put the desktop and phone on the same Tailscale / VPN overlay. See Cross-network sync with Tailscale for a comparison of both paths.
Pairing a mobile device
On the desktop, open Devices → Other ways to add → Connect a phone over LAN (the same panel that hosts the per-device list). The pairing flow:
- Pick a label for the device.
- The daemon mints a one-time username and password and shows them
inside a credentials modal with iOS / Android tabs:
- iOS — scan the QR code to install the bundled UniClipboard iOS Shortcut, which is pre-filled with the base URL and credentials.
- Android / other clients — copy the base URL, username, and password into any SyncClipboard-compatible client. For Android, install the UniClipboard Android client at github.com/UniClipboard/UniClip, with APKs in releases (side-load only, no Google Play).
- The plaintext password is shown once; close the modal and the desktop only retains an Argon2id hash. If the user loses it, use Rotate password on the device row to mint a new one.
Settings — LAN listener on/off, advertised IP and port — live behind the Mobile sync settings sheet on the same page.
What mobile sync does not do
- No iroh / no relay. The mobile listener is a separate HTTP service. There's no NAT hole-punching, no encrypted-relay fallback, no QUIC. Off-LAN clients can't reach the desktop directly — front the gateway with a headless server node (public HTTPS) or a Tailscale / VPN overlay to reach it from another network.
- No mobile ↔ mobile sync. Two phones don't talk to each other — they each talk to the desktop, and the desktop relays.
- No per-direction / per-content-type gates yet. The per-peer toggles on the desktop-peer settings sheet apply only to desktop ↔ desktop pairings.
- No participation in space history. Mobile devices don't get a copy of the encrypted space database — they read and write through the SyncClipboard endpoints in real time.
See Mobile LAN API for the wire protocol.
Recovery & reconnection
- Wi-Fi switch / laptop wake / brief outage: the daemon keeps retrying, and iroh re-establishes the QUIC session as soon as a candidate becomes reachable. No re-pairing, no user action.
- IP rotation: node IDs are stable, so the address can change and peers will still find each other through discovery on the next exchange.
- Major-version upgrade across the 0.6 boundary: the network stack was reworked, so old pairings from before 0.6 no longer resolve. Generate a new invite once and resume.
Where to look next
- Sync content — the payload types that flow over this pipeline.
- Quick start — the user-facing flow that exercises pairing end-to-end.
- CLI reference — the headless equivalents of every step described here.