Skip to main content

Overview

Connect to container terminals through WebSocket for real-time interactive shell access. The terminal WebSocket provides bidirectional communication for terminal I/O, supports terminal resizing, and includes features like tmux session persistence and collaboration.

WebSocket Endpoint

GET /ws/terminal/:containerId

Connection Parameters

containerId
string
required
Container ID (Docker ID or database UUID)
id
string
Connection identifier for multiplexing (defaults to “default”)
newSession
boolean
Create new tmux session instead of resuming (for split panes)

Authentication

WebSocket connections require authentication via:
  • JWT token in Authorization: Bearer <token> header
  • WebSocket subprotocol: rexec.v1, rexec.token.<token>
Authentication is validated before the WebSocket upgrade.

Message Format

All messages use JSON format with a type field:
interface TerminalMessage {
  type: string;  // Message type
  data?: string; // Message payload
  cols?: number; // Terminal columns (for resize)
  rows?: number; // Terminal rows (for resize)
}

Message Types

Client → Server

Input

Send keyboard input to the terminal:
{
  "type": "input",
  "data": "ls -la\n"
}

Resize

Resize the terminal dimensions:
{
  "type": "resize",
  "cols": 120,
  "rows": 40
}
Critical for TUI applications that need proper terminal dimensions.

Ping

Keep connection alive:
{
  "type": "ping"
}

Server → Client

Connected

Sent when WebSocket connection is established:
{
  "type": "connected",
  "data": "Terminal session established"
}

Output

Terminal output data:
{
  "type": "output",
  "data": "user@container:~$ "
}
Data includes ANSI escape codes for colors and formatting.

Shell Starting

Shell initialization in progress:
{
  "type": "shell_starting",
  "data": "Starting shell..."
}

Shell Ready

Shell is ready for input:
{
  "type": "shell_ready",
  "data": "Shell ready"
}

Container Status

Container status updates:
{
  "type": "container_status",
  "data": "running"
}

Error

Error messages:
{
  "type": "error",
  "data": "Container is not running"
}

Pong

Response to ping:
{
  "type": "pong"
}

Connection Flow

JavaScript Example

const containerId = "abc123...";
const token = "your-jwt-token";

// Create WebSocket connection
const ws = new WebSocket(
  `wss://api.rexec.io/ws/terminal/${containerId}`,
  ['rexec.v1', `rexec.token.${token}`]
);

ws.onopen = () => {
  console.log('Terminal connected');
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  
  switch (msg.type) {
    case 'connected':
      console.log('Session established');
      break;
      
    case 'shell_ready':
      console.log('Shell ready for input');
      // Send initial command
      ws.send(JSON.stringify({
        type: 'input',
        data: 'echo "Hello from WebSocket"\n'
      }));
      break;
      
    case 'output':
      // Display terminal output (includes ANSI codes)
      terminal.write(msg.data);
      break;
      
    case 'error':
      console.error('Terminal error:', msg.data);
      break;
  }
};

ws.onerror = (error) => {
  console.error('WebSocket error:', error);
};

ws.onclose = () => {
  console.log('Terminal disconnected');
};

// Send input to terminal
function sendInput(data) {
  ws.send(JSON.stringify({
    type: 'input',
    data: data
  }));
}

// Handle terminal resize
function resizeTerminal(cols, rows) {
  ws.send(JSON.stringify({
    type: 'resize',
    cols: cols,
    rows: rows
  }));
}

// Example: Send command
sendInput('ls -la\n');

// Example: Resize terminal
resizeTerminal(120, 40);

Browser Terminal Example with xterm.js

import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';

const term = new Terminal({
  cursorBlink: true,
  fontSize: 14,
  fontFamily: 'Menlo, Monaco, "Courier New", monospace'
});

const fitAddon = new FitAddon();
term.loadAddon(fitAddon);

// Mount terminal
term.open(document.getElementById('terminal'));
fitAddon.fit();

// Connect WebSocket
const ws = new WebSocket(
  `wss://api.rexec.io/ws/terminal/${containerId}`,
  ['rexec.v1', `rexec.token.${token}`]
);

let isShellReady = false;

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  
  if (msg.type === 'output') {
    term.write(msg.data);
  } else if (msg.type === 'shell_ready') {
    isShellReady = true;
  }
};

// Send input from terminal to container
term.onData((data) => {
  if (isShellReady && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({
      type: 'input',
      data: data
    }));
  }
});

// Handle terminal resize
term.onResize(({ cols, rows }) => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({
      type: 'resize',
      cols: cols,
      rows: rows
    }));
  }
});

// Handle window resize
window.addEventListener('resize', () => {
  fitAddon.fit();
});

Implementation Details

From Source Code

The WebSocket implementation in internal/api/handlers/terminal.go shows:
// WebSocket upgrader configuration
var upgrader = websocket.Upgrader{
    ReadBufferSize:  32 * 1024, // 32KB for large pastes
    WriteBufferSize: 32 * 1024, // 32KB for large output
    CheckOrigin: func(r *http.Request) bool {
        return true // Auth handled by middleware
    },
    HandshakeTimeout:  10 * time.Second,
    EnableCompression: true,
}

// Message types
type TerminalMessage struct {
    Type string `json:"type"` // "input", "output", "resize", "ping", "pong", "error", "connected"
    Data string `json:"data,omitempty"`
    Cols uint   `json:"cols,omitempty"`
    Rows uint   `json:"rows,omitempty"`
}

Tmux Session Persistence

Terminals use tmux for session persistence:
  • Main session: Named “main” - survives disconnections
  • Split sessions: Named “split-” - cleaned up on close
  • Control mode collab: Named “user-” - per-user sessions

Connection Optimization

Fast Path: Containers in “configuring” state use /bin/sh immediately for instant connection, with proper shell detection happening in background. Shell Detection: Prefers zsh → bash → sh based on availability. Caching: Shell detection results cached in:
  • In-memory cache for instant reconnection
  • Database for multi-replica consistency

Error Handling

Connection Errors

Container Not Found (404):
{
  "error": "container not found",
  "code": "container_not_found",
  "hint": "Container may need to be recreated. Try starting it.",
  "action_required": "start"
}
Container Stopped (400):
{
  "error": "container is not running",
  "code": "container_stopped",
  "status": "stopped",
  "hint": "Start the container before connecting to terminal",
  "action_required": "start"
}
MFA Required (423):
{
  "error": "terminal is MFA protected",
  "code": "mfa_required",
  "container_id": "abc123",
  "hint": "This terminal is protected with MFA. Enter your authenticator code to access it.",
  "action_required": "mfa_verify"
}
Access Denied (403):
{
  "error": "access denied"
}

WebSocket Close Codes

  • 1000: Normal closure
  • 4003: Collaboration ended
  • 4100: Container restart required

Features

Session Multiplexing

Multiple terminal connections per container using ?id= parameter:
const ws1 = new WebSocket(`wss://api.rexec.io/ws/terminal/${id}?id=main`);
const ws2 = new WebSocket(`wss://api.rexec.io/ws/terminal/${id}?id=split1`);

Large Data Handling

Supports messages up to 100MB for “vibe coding” scenarios:
conn.SetReadLimit(100 * 1024 * 1024)

Automatic Reconnection

Shells auto-restart when exited. macOS containers have fewer restarts due to VM nature.

Recording Support

Terminal I/O can be recorded for playback (when recording handler enabled).

Best Practices

  1. Set Initial Terminal Size: Send resize message immediately after shell_ready
  2. Handle Reconnection: Implement exponential backoff for reconnect attempts
  3. Ping/Pong: Send periodic pings (every 30s) to keep connection alive
  4. Buffer Management: Handle large output bursts efficiently
  5. ANSI Handling: Use a proper terminal emulator library (xterm.js) for ANSI codes

Collaboration Modes

View Mode

Multiple users share the same terminal session:
  • All users see the same output
  • Input from any user affects all viewers
  • Uses shared tmux session

Control Mode

Each user gets independent terminal session:
  • Own tmux session per user
  • No interference between users
  • Full control for each participant

Build docs developers (and LLMs) love