docs/remote/protocol-v2.md, and crates/core/src/remote_v2.rs is the single source of truth at the wire level. This page covers only the boundaries you must respect in an implementation.
Two pairing paths
Vibex has two pairing paths with different forms. Do not mix them:<transport> is one of direct, tailnet, or self_hosted_relay.
The offer path
- The desktop identity is generated in the runtime home and stored with
0600permissions on Unix. - An offer is cancelable and single-use, and consuming it and creating the device grant happen in the same SQLite transaction.
- An offer contains route candidates, the desktop public key, a permission summary, an expiry, and a one-time challenge. It does not contain a long-lived grant, keys, or workspace data.
- Mobile parses the link locally and strips the fragment before processing it asynchronously.
The pairing-code path
- Nine digits grouped as
NNN-NNN-NNN, derived from a UUIDv4, with only its SHA-256 hash persisted. - The client claims the code at
POST /api/v2/pairing/code/claim. The code appears only in the request body, never in the URL, so it does not reach proxies or access logs. - The endpoint shares per-peer rate limits and authentication-failure budgets with the other unauthenticated routes.
- A claim is single-use: reuse, a malformed code, or an expired code returns
remote_pairing_code_invalid/remote_pairing_code_expiredand writes apairing_code_rejectedaudit record. - A successful claim returns exactly one device grant, with the permission level fixed when the pairing code was created.
Connection strings and certificate pinning
The payload ofvibex://pair#/code/<payload> is a JSON object:
tlsCertificateDer is optional. The certificate travels out of band with the connection string (from the server console, not over the network), and the client pins it before issuing the first TLS request, so a self-signed LAN setup needs no public CA. The fingerprint format is sha256:<base64url(sha256(DER))>.
A pinned link must use https and point to a numeric LAN address (loopback, RFC1918, or link-local / unique-local IPv6); otherwise it returns remote_pairing_link_invalid. This restriction is deliberate: pairing must not become a way to bypass the public CA system.
The server identity key and the TLS certificate are pinned separately; the former comes from serverIdentityPublicKey in /api/v2/info.
Handshake and keys
The implementation uses mature primitives such as X25519, HKDF-SHA256, HMAC-SHA256, and ChaCha20-Poly1305.- A connection starts with
control/helloand completes the handshake oncontrol/server_info; the protocol uses range negotiation and currently selects2.0. - A WebSocket ticket is valid for 30 seconds and single-use, exchanged through a controlled subprotocol and not placed in a URL.
- The hello proof binds the ticket challenge, the full hello transcript, the server identity, the session epoch, the device identity, and the client ephemeral key.
server_inforeturns the server ephemeral key and the session-key confirmation.
Frame types
Unknown client, control, JSON message, attachment, binary frame, timeout, close, and transport enum values all decode to
unknown; an unknown activity message closes with a structured protocol reason instead of panicking. The legacy 0.4 HTTP and /ws routes are compatibility endpoints; new clients use /ws/v2.
RPC, events, and sync
Every business operation passes device authentication, permission, and workspace authorization. Mutations use bounded idempotency keys; file operations additionally use content revisions / CAS. When a client detects a sequence-number or cursor gap, it returnsresync_required and points at an authoritative operation to re-fetch. Attachment streams re-authenticate and re-authorize their domain before they start. A terminal attachment carries workspace scope, and terminal input requires generation checks, authorization, and auditing, and keeps no raw bytes.
After a device grant is revoked, every active connection for that device immediately receives device_revoked. When the runtime shuts down, it sends server_shutdown, drains listeners, and releases sockets.
Permission model
RemoteDevicePermissionLevel has three values, serialized as read_only / approve_only / full_control:
The level is fixed on the authority when the offer / pairing code is created; a client cannot raise its own privileges.
Gateway endpoints
Relay constraints
Relay is a zero-knowledge room router: it only forwards opaque encrypted frames. It may expose the routing metadata that/health, /api/info, and the WebSocket bridge need, but it does not perform business authorization, read or write workspaces, or store provider profiles or timelines.
The endpoints on Relay are /health, /api/info, /ws, /api/rooms/{room_id}/pair|command, and two /api/push/* routes. There is no /api/v2/info on Relay — that is an endpoint of the authoritative runtime.
Local checks:
Connection strategy
The client can probe and fall back between Direct, Tailnet, Relay, and LAN. After a route recovers, probe again and switch, without pairing again; if the device has been revoked, every route must fail.Deployment
docs/smoke/relay-nat.md and docs/smoke/remote-lan.md in the repository.