API and protocols
All REST paths below are relative to the API origin. Platform routes use a normal platform Bearer JWT. Device routes use a runner-audience JWT unless the row explicitly states dual authentication.
REST surface
Expert and platform routes
| Method and path | Authorization | Contract |
|---|---|---|
GET /v1/remote-desktop/machines/mine | Expert with org access, or Superadmin | Entitled assigned machines; includes protected VNC credentials for the caller's usable machine |
GET /v1/remote-desktop/sessions?orgId=<uuid> | Same org authorization | Latest 50 sessions, newest first |
POST /v1/remote-desktop/connect | Same org authorization | Body { "orgId": "<uuid>" }; returns the server transport plan |
POST /v1/remote-desktop/sessions/:id/end | Session-owner platform JWT or runner JWT for the same org | Idempotently ends a direct session |
POST /v1/remote-desktop/webrtc/telemetry | Valid platform JWT; 120 requests/minute | Best-effort browser telemetry; returns 204 |
machines/mine exposes placement and display information, not device network
coordinates. Its machine object may include vncUsername and vncCredential
because noVNC must authenticate to macOS Screen Sharing. Those values are not
returned by the operations fleet endpoint.
Device routes
| Method and path | Authorization | Contract |
|---|---|---|
GET /v1/remote-desktop/webrtc/ice-servers | Machine-bound runner JWT | STUN plus short-lived TURN configuration for an active assigned machine |
POST /v1/remote-desktop/validate-token | Runner guard | Consumes { token, machineId } once and returns { ok: true, sessionId } |
POST /v1/remote-desktop/machines/:id/tunnel-health | Runner guard for the same org | Body `{ "healthy": true |
POST /v1/remote-desktop/sessions/:id/end | Runner JWT for the session org | Body may include closeReason |
Operations routes
All operations routes enforce MFA and platform roles.
| Method and path | Roles | Purpose |
|---|---|---|
GET /admin/machines | Superadmin, Account Manager | Fleet overview; AM network details are redacted |
GET /admin/machines/suggestions?orgId=<uuid> | Superadmin | Jurisdiction-ranked pooled inventory |
POST /admin/machines | Superadmin | Create an unassigned pooled machine |
PATCH /admin/machines/:id | Superadmin | Edit placement, labels, connection config or metadata |
POST /admin/machines/:id/assign | Superadmin | Body { orgId, reason? } |
POST /admin/machines/:id/unassign | Superadmin | Return to pool; optional reason |
POST /admin/machines/:id/maintenance | Superadmin | Enter maintenance; optional reason |
POST /admin/machines/:id/retire | Superadmin | Retire; optional reason |
POST /admin/machines/:id/restore | Superadmin | Restore; optional reason |
GET /admin/machines/:id/assignments | Superadmin | Append-only assignment history |
POST /admin/machines/:orgId/computer-access | Superadmin | Body `{ "enabled": true |
POST /admin/machines/:id/connector-token | Superadmin | Mint a machine-bound connector JWT; 503 if RUNNER_JWT_SECRET is absent |
Create accepts tailscaleHostname and optional vncPort, vmName, placement,
labels, connectMode, publicHostname, and metadata. Update accepts the same
mutable fields except tailscaleHostname. Ports are restricted to 1-65535;
connect mode is one of proxy, direct_wss, or webrtc.
Connect response
The response is a discriminated union on mode.
WebRTC plan
{
"mode": "webrtc",
"signalingUrl": "wss://api.example/v1/remote-desktop/signal?...",
"iceServers": [{ "urls": ["stun:..."] }],
"iceServersExpiresAt": null,
"session": { "orgId": "...", "machineId": "..." },
"connector": { "machineId": "...", "role": "browser" },
"vncUsername": "...",
"vncCredential": "...",
"fallback": {
"mode": "p2p_rfb",
"signalingUrl": "wss://api.example/v1/remote-desktop/signal?...",
"iceServers": [],
"iceServersExpiresAt": null,
"fallback": {
"mode": "proxy",
"wsUrl": "wss://api.example/v1/remote-desktop?token=...&orgId=...",
"iceServers": [],
"iceServersExpiresAt": null
}
}
}
Credential values are nullable. The browser first attempts native video, then uses the P2P descriptor for RFB, then its nested relay plan.
Direct WSS plan
{
"mode": "direct",
"sessionId": "...",
"wssUrl": "wss://machine.example/vnc?token=<single-use>",
"expiresAt": "...",
"vncUsername": "...",
"vncCredential": "...",
"iceServers": [],
"iceServersExpiresAt": null,
"fallback": {
"mode": "proxy",
"wsUrl": "wss://api.example/v1/remote-desktop?token=...&orgId=..."
}
}
The default token lifetime is 60 seconds and is capped at one hour. The direct plan is issued only when the machine tunnel is healthy and Redis can store the single-use token.
Proxy plan
{
"mode": "proxy",
"wsUrl": "wss://api.example/v1/remote-desktop?token=...&orgId=...",
"vncUsername": "...",
"vncCredential": "...",
"iceServers": [],
"iceServersExpiresAt": null
}
The URL contains a live platform JWT. Do not log or persist the response.
WebSocket endpoints
Raw relay: /v1/remote-desktop
This exact path is a raw RFC 6455 WebSocket, not Socket.IO. The platform JWT is
accepted in the token query parameter or as a JWT-like WebSocket subprotocol;
orgId selects the authorized organization. The negotiated data subprotocol
may be binary or base64, although the production noVNC path uses binary
RFB bytes.
Authentication failure closes with application code 4001; authorization
failure closes with 4003. An upstream TCP failure closes with 1011.
Signaling: /v1/remote-desktop/signal
Browser URL parameters are role=browser, platform token, and orgId.
Connector parameters are role=agent and the machine-bound runner token.
The browser is the offerer and the connector is the answerer.
After browser authentication, the server creates an audit row and sends:
{ "type": "ready", "sessionId": "<server-assigned>" }
Subsequent accepted frames are at most 64 KiB and use one of:
{ "type": "offer", "sessionId": "...", "sdpType": "offer", "sdp": "..." }
{ "type": "answer", "sessionId": "...", "sdpType": "answer", "sdp": "..." }
{ "type": "ice", "sessionId": "...", "cand": "...", "mid": "..." }
{ "type": "bye", "sessionId": "..." }
Malformed JSON, unknown types and oversized frames are dropped.
The server replaces every browser-supplied sessionId with its assigned value,
so one browser cannot address another session. Connector-to-browser replies
are constrained by the connector's authenticated machine binding.
Redis pub/sub carries cross-pod signaling on
rdwebrtc:machine:<machineId> and rdwebrtc:session:<sessionId>. With no Redis,
delivery is in-process and correct only when browser and connector land on the
same single API pod.
WebRTC media and DataChannels
The negotiated video codec is H.264 with packetization mode 1. The browser prefers H.264 and validates the reported negotiated codec when the browser exposes it. The connector publishes a send-only track; the browser receives only. ICE configuration comes from the connect plan and the connector's authenticated ICE endpoint.
The connector accepts only the allowlisted channel labels below. Only the first
input channel is accepted; duplicates are closed.
| Label | Direction | Payload |
|---|---|---|
input | Browser to connector | Reliable ordered UTF-8 JSON input and quality control; maximum input event frame 512 bytes |
rfb | Bidirectional | Raw RFB byte stream bridged to guest VNC; fallback path |
Input protocol v1
Coordinates are normalized to encoded video content, not the CSS video box. The connector maps them to the current display's Quartz bounds and physical pixels, including Retina displays.
| Event | Shape | Limits |
|---|---|---|
| Move/drag | {"v":1,"t":"pm","x":0.5,"y":0.25,"m":0} | x,y in [0,1]; optional DOM buttons mask m in 0..7 |
| Pointer down/up | {"v":1,"t":"pd","x":0.5,"y":0.25,"b":0,"n":1} | t is pd or pu; button 0..2; optional click count 1..3 |
| Scroll | {"v":1,"t":"sc","x":0.5,"y":0.25,"dx":0,"dy":24} | Each axis -120..120 |
| Key down/up | {"v":1,"t":"kd","c":"KeyA"} | t is kd or ku; explicit KeyboardEvent.code allowlist |
Compact events may include modifier mask f in 0..15 for Shift, Control,
Alt and Command. The connector also accepts the browser's strict verbose
physical-video-pixel envelope and normalizes it into the same internal model.
It is not a generic JSON input path.
Valid events are limited to 240 per second with a burst of 120. Unknown fields, trailing JSON, invalid values, unknown event types and binary input are dropped without consuming rate capacity. The native boundary checks Accessibility for every event and fails closed.
The input gate opens only after an authenticated offer is accepted. A transient disconnect closes the gate and releases held keys/buttons; it may reopen only for the same accepted session. Terminal teardown closes it permanently before the peer or channel is destroyed.
Session lifecycle and close semantics
- Relay sessions start
activewhen the API relay opens. - WebRTC sessions start
activewhen the browser signaling socket is accepted. - Direct WSS sessions start
pending, becomeactivewhen the device consumes the token, and becomeendedon close. - Browser and device end calls are idempotent. The first update sets
ended_at,close_reason, andstatus='ended'. - Browser signaling close records
client_closed; connector close recordsdevice_closed. Relay failures use bounded reasons such asupstream_error,upstream_closed,client_closed, orclient_error.
Telemetry payload
The browser posts kind: periodic|disconnect with optional reason, rttMs,
jitterMs, freezeCount, sessionSeconds, candidateType, and
relayProtocol. Numeric values are clamped and strings are mapped to bounded
labels before entering Prometheus. Telemetry is advisory, fire-and-forget, and
must never delay or fail a desktop session.