Skip to content

Remote Access

Breeze provides three distinct real-time access capabilities for managed devices: Remote Desktop (interactive screen viewing and control), Remote Terminal (browser-based interactive shell), and File Transfer (bi-directional file movement between the technician and the device). Each is a separate session type created through the same /remote/sessions API.

Remote desktop sessions use WebRTC as the primary transport. The Breeze server acts as a signaling relay during connection setup — it brokers the initial SDP offer/answer exchange and ICE candidate negotiation between the native viewer app and the device agent. Once the peer-to-peer connection is established, video, audio, and input data flow directly between the viewer and the agent and do not transit the Breeze server.

The Breeze stack includes a coturn TURN server that relays WebRTC traffic when a direct peer-to-peer connection is not possible (symmetric NAT, restrictive firewalls). The API generates short-lived TURN credentials via a shared secret and distributes them to both the viewer and agent during ICE negotiation. See Environment Variables for configuration.

If WebRTC negotiation fails even with TURN (e.g., TURN is misconfigured or unreachable), the viewer automatically falls back to a WebSocket transport that relays JPEG frames through the Breeze server. The WebSocket fallback has reduced capabilities compared to WebRTC — no audio, no clipboard sync, no multi-monitor switching, and no low-latency cursor streaming.


Remote Desktop streams the device’s screen to your browser and forwards your mouse and keyboard input back to the device in real time, giving you full interactive control as if you were physically present at the machine.

  1. Navigate to Devices, locate the target device, and open its detail page.

  2. Click Connect and select Desktop.

  3. A fullscreen viewer opens once the WebRTC connection is active.

  4. Mouse movement and clicks are captured and relayed to the device in real time.

  5. Keyboard input is forwarded directly — use the Release Input button or press Escape (if configured) to return control to your browser.

The browser and device negotiate a WebRTC peer-to-peer connection through the Breeze server:

  1. POST /remote/sessions with type: "desktop" — creates the session record.
  2. GET /remote/ice-servers — retrieve STUN/TURN server configuration.
  3. POST /remote/sessions/:id/offer — send the SDP offer from the browser.
  4. POST /remote/sessions/:id/answer — relay the device’s SDP answer back.
  5. POST /remote/sessions/:id/ice — exchange ICE candidates.
  6. POST /remote/sessions/:id/ws-ticket — mint a one-time WebSocket ticket.
  7. Connect via WebSocket using the ticket.

Once the P2P connection is active, screen data flows directly between the browser and the agent.

When the Breeze agent runs as a Windows service, it operates in Session 0 — an isolated session with no interactive desktop. Standard screen capture APIs (DXGI, GDI) and input injection all fail in Session 0.

Breeze solves this with a session broker architecture:

  1. The agent service detects it is running in Session 0 via config.IsService.

  2. The session broker enumerates active user sessions using the Windows Terminal Services API (WTS).

  3. When a remote desktop session is requested, the broker spawns a SYSTEM-level helper process into the target user’s session using CreateProcessAsUser with session ID override.

  4. The helper process owns the entire WebRTC pipeline — DXGI screen capture, H264 encoding via Media Foundation, and WebRTC for streaming. The service only relays signaling messages (SDP offer/answer) over IPC.

  5. When the session ends, the helper process is torn down.

Windows implementation details

The helper uses a SYSTEM token (not WTSQueryUserToken) intentionally. SYSTEM can call OpenInputDesktop(GENERIC_ALL) which grants access to the UAC consent desktop and lock screen — something a user-level token cannot do.

The list_sessions command returns available user sessions by merging the WTS session detector output with the broker’s helper state, showing which sessions have active remote desktop connections.

The same session broker architecture extends to macOS and Linux. When the agent runs as a LaunchDaemon (macOS) or systemd service (Linux), it has no display access. The agent detects this via the IsHeadless flag (checked by hasConsole() on Unix).

In headless mode, all desktop and screenshot commands are routed through IPC to the Breeze Helper process running in the user’s desktop session, rather than attempting direct screen capture which would silently fail. The Helper handles screen capture using platform-native APIs (Core Graphics on macOS, X11/Wayland on Linux).

Breeze supports multi-monitor remote desktop. The viewer can switch between displays on the target device. Each display is captured independently using DXGI Desktop Duplication.

The remote desktop pipeline uses adaptive bitrate control to maintain smooth streaming across varying network conditions. The encoder adjusts the H264 bitrate based on:

  • Measured network throughput between viewer and agent
  • Frame delivery latency
  • Packet loss rate

The bitrate ramps up when conditions are good and drops when congestion is detected. A stability threshold of 2 consecutive stable measurements is required before increasing bitrate to prevent oscillation.

Remote desktop sessions can optionally stream system audio from the device to the viewer. Audio is captured from the device’s default audio output and transmitted over a separate WebRTC audio track. Audio streaming is enabled by default on Windows.

Breeze can capture the Windows secure desktop (Ctrl+Alt+Del screen, UAC prompts, lock screen). This is possible because the helper process runs as SYSTEM with GENERIC_ALL access to the input desktop. Secure desktop capture is automatic — no additional configuration is required.


Remote Terminal opens an interactive shell session rendered in the browser using xterm.js. It is a PTY session on the device — keystrokes are sent to the device’s shell and output is streamed back in real time, with full terminal emulation including colour, cursor control, and resizing.

  1. Navigate to Devices, locate the target device, and open its detail page.

  2. Click Connect and select Terminal.

  3. A browser terminal window opens. Wait for the connected indicator in the toolbar before typing — the terminal is ready only after the WebRTC P2P connection is fully established and the agent confirms the PTY is open.

  4. Resizing the browser window (or the terminal pane) automatically adjusts the PTY dimensions on the device.

The connection flow is identical to Remote Desktop:

  1. POST /remote/sessions with type: "terminal" — creates the session record.
  2. GET /remote/ice-servers — retrieve STUN/TURN configuration.
  3. POST /remote/sessions/:id/offer — send the SDP offer.
  4. POST /remote/sessions/:id/ws-ticket — mint the WebSocket ticket.
  5. Connect via WebSocket using the ticket.

File Transfer moves files between the technician’s browser and the device. It is bi-directional: you can pull files off a device (for example, log files or crash dumps) or push files to a device (for example, scripts or configuration files).

Direction is expressed from the device’s perspective:

  • upload — the device sends a file to you. Use this to pull a file from the device.
  • download — you send a file to the device. Use this to push a file to the device.

When in doubt: to retrieve /var/log/app.log from a device, use "direction": "upload".

Terminal window
POST /remote/transfers
Content-Type: application/json
{
"deviceId": "uuid",
"direction": "upload",
"remotePath": "/var/log/app.log",
"localFilename": "app.log",
"sizeBytes": 1048576
}
FieldDescription
deviceIdUUID of the target device
direction"upload" (device → you) or "download" (you → device)
remotePathFull path of the file on the device
localFilenameFilename used when the file is delivered to the browser
sizeBytesSize in bytes (required for progress tracking)

Poll GET /remote/transfers/:id:

StatusMeaning
pendingTransfer created, waiting for the device agent to begin
transferringData is actively moving
completedTransfer finished successfully
failedTransfer encountered an unrecoverable error (check errorMessage)

The response also includes progressPercent (0–100).

For upload direction transfers (device → you), once status is completed:

GET /remote/transfers/:id/download

Returns the file as a binary response. Only available for upload direction transfers.

POST /remote/transfers/:id/cancel

Accepted only while the transfer is in pending or transferring state. A cancelled transfer lands in failed status with errorMessage: "Cancelled by user" — check errorMessage to distinguish user cancellation from a genuine error.

The default maximum file size per transfer is 500 MB (configurable via MAX_TRANSFER_SIZE_MB). Transfers exceeding this limit are rejected when chunk data pushes the total past the threshold.


All session types follow the same state machine:

pending → connecting → active → disconnected / failed
StateDescription
pendingSession record created; device agent has not yet acknowledged the request
connectingDevice acknowledged; SDP offer/answer exchange and ICE negotiation in progress
activeWebRTC P2P connection established; data flowing between browser and device
disconnectedSession ended normally — user closed it or the agent reported a clean disconnect
failedConnection could not be established, or an active connection dropped unexpectedly

When a session ends (disconnected or failed), the platform records durationSeconds (calculated from start time). The bytesTransferred field is available but must be supplied by the caller when ending the session via POST /remote/sessions/:id/end.


Breeze enforces per-organisation and per-user limits on concurrent remote access resources. When a limit is reached, the creation request returns 429 Too Many Requests.

ResourcePer-orgPer-user
Active sessions105
Active file transfers2010
Max transfer size500 MB500 MB

These defaults are configurable via environment variables on the API server:

VariableDefault
MAX_ACTIVE_REMOTE_SESSIONS_PER_ORG10
MAX_ACTIVE_REMOTE_SESSIONS_PER_USER5
MAX_ACTIVE_TRANSFERS_PER_ORG20
MAX_ACTIVE_TRANSFERS_PER_USER10
MAX_TRANSFER_SIZE_MB500

To free session capacity, close active sessions from the Active Sessions list on the device detail page, or call DELETE /remote/sessions/stale — this marks pending, connecting, and active sessions as disconnected. Be aware this terminates any currently active sessions, not just stale ones.


MFA required for all remote access. Every session creation request is gated by MFA verification at the route level. A valid MFA check must be present before a remote session can be created.

Server is a signaling relay for WebRTC. The Breeze server brokers WebRTC negotiation (SDP offer/answer and ICE candidates). Once the P2P connection is established, device data does not transit the server. If the WebSocket fallback is active, frame data is relayed through the server but is not persisted.

All sessions are audit logged. Every session creation, state transition, and termination is recorded with the actor identity, device identifier, start time, end time, duration in seconds, and bytes transferred. Audit records are immutable.

Remote access requires the remote:access permission. This permission is not granted by default on all roles. Navigate to Settings → Roles if a user receives “Access denied” when creating a session.


MethodPathDescription
POST/remote/sessionsCreate a session (body: deviceId, type)
GET/remote/sessionsList sessions (?deviceId=&status=&type=&includeEnded=)
GET/remote/sessions/:idGet session details
GET/remote/sessions/historySession statistics (total sessions, total duration, average duration)
DELETE/remote/sessions/staleTerminate all pending, connecting, and active sessions
GET/remote/ice-serversGet ICE/TURN server configuration for WebRTC
POST/remote/sessions/:id/offerSend SDP offer from browser to device
POST/remote/sessions/:id/answerRelay device’s SDP answer back to browser
POST/remote/sessions/:id/iceExchange ICE candidates between browser and device
POST/remote/sessions/:id/ws-ticketMint a one-time WebSocket ticket (required for terminal and desktop)
POST/remote/sessions/:id/endEnd a session
MethodPathDescription
POST/remote/transfersInitiate a transfer
GET/remote/transfersList transfers (?deviceId=&status=&direction=)
GET/remote/transfers/:idGet transfer details and current progress
POST/remote/transfers/:id/cancelCancel a pending or active transfer
POST/remote/transfers/:id/chunksUpload a chunk (multipart: chunkIndex, data)
GET/remote/transfers/:id/downloadDownload a completed file (upload direction only)

Session stuck in connecting. WebRTC negotiation failed and the viewer has not yet fallen back to WebSocket. Confirm the device is online and the agent WebSocket connection is active (check the device detail page). Common causes:

  • TURN not configured — Verify TURN_HOST and TURN_SECRET are set in .env and the coturn container is running (docker compose logs coturn). If using an external TURN server, check its status via SSH.
  • Firewall blocking TURN ports — Coturn needs TCP and UDP port 3478, plus UDP ports in the relay range (default 49152–65535, or tightened to 49152–49252). Verify the ports are reachable with nc -z -w 3 TURN_IP 3478 (TCP) and nc -u -z -w 3 TURN_IP 3478 (UDP).
  • Wrong TURN_HOST — Must be the TURN server’s public IP, not a private address. If Breeze is behind Cloudflare Tunnel, TURN must run on a separate host with its own public IP — see TURN Server.
  • Shared secret mismatchTURN_SECRET in .env must exactly match static-auth-secret in the coturn config.

The viewer should automatically fall back to the WebSocket transport after a timeout.

Session limit hit (429). The organisation or user limit on concurrent sessions has been reached. Call DELETE /remote/sessions/stale to free capacity, or navigate to the Active Sessions list and close sessions that are no longer needed.

Terminal garbled after resize. This was a race condition in older builds where the browser sent a PTY resize message before the server connected acknowledgement was received. Current builds wait for the connected message before sending any data, including resize events. Update the agent to a current release.

File transfer stuck at 0%. The device must be online with an active WebSocket connection for the transfer to begin. If the device disconnected after the transfer was created, cancel it (POST /remote/transfers/:id/cancel) and retry once the device reconnects.

“Access denied” when creating a session. The user account does not have the remote:access permission. Navigate to Settings → Roles, locate the role assigned to the user, and enable remote:access. Changes take effect immediately without requiring re-login.