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
Container ID (Docker ID or database UUID)
Connection identifier for multiplexing (defaults to “default”)
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.
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
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:
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:
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
- Set Initial Terminal Size: Send resize message immediately after
shell_ready
- Handle Reconnection: Implement exponential backoff for reconnect attempts
- Ping/Pong: Send periodic pings (every 30s) to keep connection alive
- Buffer Management: Handle large output bursts efficiently
- 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