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
Section titled “Remote Desktop”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.
Starting a desktop session
Section titled “Starting a desktop session”-
Navigate to Devices, locate the target device, and open its detail page.
-
Click Connect and select Desktop.
-
A fullscreen viewer opens once the WebRTC connection is active.
-
Mouse movement and clicks are captured and relayed to the device in real time.
-
Keyboard input is forwarded directly — use the Release Input button or press Escape (if configured) to return control to your browser.
Connection flow
Section titled “Connection flow”The browser and device negotiate a WebRTC peer-to-peer connection through the Breeze server:
POST /remote/sessionswithtype: "desktop"— creates the session record.GET /remote/ice-servers— retrieve STUN/TURN server configuration.POST /remote/sessions/:id/offer— send the SDP offer from the browser.POST /remote/sessions/:id/answer— relay the device’s SDP answer back.POST /remote/sessions/:id/ice— exchange ICE candidates.POST /remote/sessions/:id/ws-ticket— mint a one-time WebSocket ticket.- Connect via WebSocket using the ticket.
Once the P2P connection is active, screen data flows directly between the browser and the agent.
Windows Session 0 Support
Section titled “Windows Session 0 Support”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:
-
The agent service detects it is running in Session 0 via
config.IsService. -
The session broker enumerates active user sessions using the Windows Terminal Services API (WTS).
-
When a remote desktop session is requested, the broker spawns a SYSTEM-level helper process into the target user’s session using
CreateProcessAsUserwith session ID override. -
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.
-
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.
macOS and Linux Headless Support
Section titled “macOS and Linux Headless Support”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).
Multi-Display Support
Section titled “Multi-Display Support”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.
Adaptive Bitrate
Section titled “Adaptive Bitrate”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.
Audio Streaming
Section titled “Audio Streaming”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.
Secure Desktop Capture
Section titled “Secure Desktop Capture”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
Section titled “Remote Terminal”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.
Starting a terminal session
Section titled “Starting a terminal session”-
Navigate to Devices, locate the target device, and open its detail page.
-
Click Connect and select Terminal.
-
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.
-
Resizing the browser window (or the terminal pane) automatically adjusts the PTY dimensions on the device.
Connection flow
Section titled “Connection flow”The connection flow is identical to Remote Desktop:
POST /remote/sessionswithtype: "terminal"— creates the session record.GET /remote/ice-servers— retrieve STUN/TURN configuration.POST /remote/sessions/:id/offer— send the SDP offer.POST /remote/sessions/:id/ws-ticket— mint the WebSocket ticket.- Connect via WebSocket using the ticket.
File Transfer
Section titled “File Transfer”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 convention
Section titled “Direction convention”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".
Initiating a transfer
Section titled “Initiating a transfer”POST /remote/transfersContent-Type: application/json
{ "deviceId": "uuid", "direction": "upload", "remotePath": "/var/log/app.log", "localFilename": "app.log", "sizeBytes": 1048576}| Field | Description |
|---|---|
deviceId | UUID of the target device |
direction | "upload" (device → you) or "download" (you → device) |
remotePath | Full path of the file on the device |
localFilename | Filename used when the file is delivered to the browser |
sizeBytes | Size in bytes (required for progress tracking) |
Tracking progress
Section titled “Tracking progress”Poll GET /remote/transfers/:id:
| Status | Meaning |
|---|---|
pending | Transfer created, waiting for the device agent to begin |
transferring | Data is actively moving |
completed | Transfer finished successfully |
failed | Transfer encountered an unrecoverable error (check errorMessage) |
The response also includes progressPercent (0–100).
Downloading a completed file
Section titled “Downloading a completed file”For upload direction transfers (device → you), once status is completed:
GET /remote/transfers/:id/downloadReturns the file as a binary response. Only available for upload direction transfers.
Cancelling a transfer
Section titled “Cancelling a transfer”POST /remote/transfers/:id/cancelAccepted 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.
Size limit
Section titled “Size limit”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.
Session Lifecycle
Section titled “Session Lifecycle”All session types follow the same state machine:
pending → connecting → active → disconnected / failed| State | Description |
|---|---|
pending | Session record created; device agent has not yet acknowledged the request |
connecting | Device acknowledged; SDP offer/answer exchange and ICE negotiation in progress |
active | WebRTC P2P connection established; data flowing between browser and device |
disconnected | Session ended normally — user closed it or the agent reported a clean disconnect |
failed | Connection 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.
Rate Limits
Section titled “Rate Limits”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.
| Resource | Per-org | Per-user |
|---|---|---|
| Active sessions | 10 | 5 |
| Active file transfers | 20 | 10 |
| Max transfer size | 500 MB | 500 MB |
These defaults are configurable via environment variables on the API server:
| Variable | Default |
|---|---|
MAX_ACTIVE_REMOTE_SESSIONS_PER_ORG | 10 |
MAX_ACTIVE_REMOTE_SESSIONS_PER_USER | 5 |
MAX_ACTIVE_TRANSFERS_PER_ORG | 20 |
MAX_ACTIVE_TRANSFERS_PER_USER | 10 |
MAX_TRANSFER_SIZE_MB | 500 |
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.
Security
Section titled “Security”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.
API Reference
Section titled “API Reference”Sessions
Section titled “Sessions”| Method | Path | Description |
|---|---|---|
| POST | /remote/sessions | Create a session (body: deviceId, type) |
| GET | /remote/sessions | List sessions (?deviceId=&status=&type=&includeEnded=) |
| GET | /remote/sessions/:id | Get session details |
| GET | /remote/sessions/history | Session statistics (total sessions, total duration, average duration) |
| DELETE | /remote/sessions/stale | Terminate all pending, connecting, and active sessions |
| GET | /remote/ice-servers | Get ICE/TURN server configuration for WebRTC |
| POST | /remote/sessions/:id/offer | Send SDP offer from browser to device |
| POST | /remote/sessions/:id/answer | Relay device’s SDP answer back to browser |
| POST | /remote/sessions/:id/ice | Exchange ICE candidates between browser and device |
| POST | /remote/sessions/:id/ws-ticket | Mint a one-time WebSocket ticket (required for terminal and desktop) |
| POST | /remote/sessions/:id/end | End a session |
File Transfers
Section titled “File Transfers”| Method | Path | Description |
|---|---|---|
| POST | /remote/transfers | Initiate a transfer |
| GET | /remote/transfers | List transfers (?deviceId=&status=&direction=) |
| GET | /remote/transfers/:id | Get transfer details and current progress |
| POST | /remote/transfers/:id/cancel | Cancel a pending or active transfer |
| POST | /remote/transfers/:id/chunks | Upload a chunk (multipart: chunkIndex, data) |
| GET | /remote/transfers/:id/download | Download a completed file (upload direction only) |
Troubleshooting
Section titled “Troubleshooting”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_HOSTandTURN_SECRETare set in.envand 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) andnc -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 mismatch —
TURN_SECRETin.envmust exactly matchstatic-auth-secretin 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.