Pairing
Your user has the app on their phone and installs it on a new tablet. The tablet needs to join the phone’s group, and nobody else should be able to slip in while that happens. The relay passes the messages between the two devices but never sees the Pairing Token, so the token is where both devices anchor their trust.
Admission is explicit on both sides
The admitting device shows a Pairing Token. It holds the admitting device’s public keys and a random, single-use Pairing Secret. The joining device reads the token through a channel the relay never sees, such as a QR code on the screen.
- The admitting user accepts one join request by hand. A join request has to prove it knows the Pairing Secret, so the relay can’t make one up.
- The joining device trusts only the signing key in the token it read. It accepts its first Group Manifest only if that key signed it and the joining device’s own keys are in the member list, so the relay can’t put it into a group of the relay’s choosing.
A device that is in no group drops every Sync and Revoke message it receives, and accepts a Group Manifest only while it has a Pending Join.
The admitting device
- Call
startPairing(). It opens the pairing window, makes a fresh Pairing Secret, and returns the Pairing Token as a string. Show it, for example as a QR code. - Each join request arrives as a
joinRequest(request)event, with anidand aname. If one device sends its request more than once, you get it once. Requests from different devices can arrive while the window is open, and the user picks one. - Call
accept(request). Before it returns, the library saves the new Group Manifest and queues it for delivery to every member, the new device included. Then the window closes.
If this device wasn’t in a group yet, the first accept(request) starts a new group with both devices in it.
accept(request) fails with pairingClosed when the window is no longer open, and with groupFull{max} when the group already has the Maximum Group Size of 32 members. A window admits one device. To add another, call startPairing() again, which makes a new token.
There is no reject call, because the protocol sends no decline. A request you don’t accept gets no reply, and it disappears when the window closes. cancelPairing() closes the window.
The window has no timer. It stays open until accept(request) succeeds or you call cancelPairing(), so close it when the user leaves your pairing screen.
The joining device
- Call
join(token)with the token the user scanned. The library saves a Pending Join and queues the join request in its outbox, both in one write.statusbecomespendingJoin. - The device waits. A Pending Join has no timer, survives a restart, and keeps its join request in the outbox until the relay has accepted it.
- When a valid Group Manifest arrives, the library stores it and clears the Pending Join in one write.
statusbecomesmember, and onestatusChanged(member, joined)event fires.
join(token) fails with alreadyInGroup when the device is already in a group, since one namespace holds one Sync Group. It fails with invalidPairingToken when the token can’t be read.
Calling join(token) again with a new token replaces the Pending Join. cancelJoin() clears it and returns the device to notJoined.
Join states
status tells you where the device is at any time. Drive your pairing UI from it.
notJoined: in no group and not waiting to join.pendingJoin: waiting for the admitting device to accept.member: in the group. Membership and Destroy Group say what moves it from there.
| From | Trigger | To |
|---|---|---|
notJoined | join(token) | pendingJoin |
pendingJoin | join(token) with another token | Unchanged. The Pending Join is replaced. |
pendingJoin | cancelJoin() | notJoined |
pendingJoin | A valid Group Manifest | member. statusChanged(member, joined) fires once. |
pendingJoin | A Group Manifest that fails the checks | Unchanged. The manifest is dropped and the Pending Join kept. |
member, leaving, destroying | join(token) | Unchanged. Fails with alreadyInGroup. |
status has two more values, leaving and destroying. A device reaches them only after it has been a member, as the Membership and Destroy Group pages describe.
The pairing window on the admitting device is separate from status. It is open from startPairing() until accept(request) succeeds, cancelPairing() runs, or the app restarts.
Failure and restart
- The joining device restarts while pending. The Pending Join survives. The library doesn’t send a second join request on restart; the outbox keeps retrying the first one until the relay accepts it.
- The admitting app restarts, or calls
cancelPairing(). The window closes and its Pairing Secret is spent. Pending requests are forgotten, since they live in memory only. A join request that arrives later under that secret is dropped. - The joining device holds a token from a closed window. It stays in
pendingJoinand nothing arrives. Its app callscancelJoin(), or the user scans a new token and the app callsjoin(token)again. - The admitting user never accepts. The joining device gets no reply. Same as above.
- A Group Manifest arrives that the token’s key didn’t sign, or that leaves this device out. The library drops it and keeps the Pending Join.
- The joining device crashes during
join(token). Before the write completes, the device is stillnotJoined, and the app callsjoin(token)again. After it, the device ispendingJoin. - The joining device crashes while storing the manifest. The relay delivers the manifest again, because the device hadn’t acknowledged it.
- The admitting device crashes before
accept(request)completes. The window is lost and no one joined. The joining device stays pending until its app cancels or the user scans a new token. - The admitting device crashes after
accept(request)completes. The outbox sends the queued manifest after restart.
A Pending Join with no answer stays pending forever. Give the user a way out, such as a cancel button that calls cancelJoin().
What stays your app’s job
- Showing the token, and keeping the pairing screen open while the user scans it.
- Confirming the device. Anyone who sees the Pairing Token while the window is open can send a join request with it. The
namein a request is a label the joining device chose, so it doesn’t prove which device sent it. Your app should ask the user to confirm that the request comes from the device in front of them before it callsaccept(request). - Closing the window with
cancelPairing()when the user leaves the pairing screen. - A way to cancel a Pending Join.
- History. A new member receives only the messages other members send after they learn it joined. If it needs earlier data, your app sends it.
TrueSeal has no short authentication string, so the two screens never show a matching code to compare. Pairing relies on the token being seen only by the people in the room.