# How to Implement a RemoteTransport for Nodeterm: WebSocket PTY Integration

> Implement a RemoteTransport for nodeterm by routing PTY calls over WebSocket. Learn how to integrate with a remote server for seamless terminal functionality.

- Repository: [eneskirca/nodeterm](https://github.com/eneskirca/nodeterm)
- Tags: how-to-guide
- Published: 2026-08-26

---

**To implement a RemoteTransport for nodeterm, create a class that implements the `TerminalTransport` interface in [`src/renderer/terminal/transport.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/terminal/transport.ts) and routes PTY lifecycle calls over WebSocket to a remote server instead of Electron's IPC.**

Nodeterm's architecture deliberately decouples terminal rendering from process creation through the `TerminalTransport` abstraction. While the default `LocalTransport` in [`src/renderer/terminal/local-transport.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/terminal/local-transport.ts) communicates with Electron's main process via IPC, you can **implement a RemoteTransport for nodeterm** by creating a network-capable class that satisfies the same interface contract.

## Understanding the TerminalTransport Abstraction

The `TerminalTransport` interface defined in [`src/renderer/terminal/transport.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/terminal/transport.ts) serves as the contract between the React UI and the underlying PTY (pseudo-terminal) provider. This abstraction is intentionally free of Electron-specific imports, allowing the same renderer code to run in both desktop and server environments.

The current `LocalTransport` implementation resides in [`src/renderer/terminal/local-transport.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/terminal/local-transport.ts) and uses `window.nodeTerminal` to proxy calls to the main process. To support remote scenarios—such as browser-based server editions or cloud-hosted agents—you must provide an alternative implementation that satisfies the same interface but communicates over network protocols like WebSocket.

## The TerminalTransport Interface Contract

According to the source code in [`src/renderer/terminal/transport.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/terminal/transport.ts), any transport implementation must expose five core methods that manage the PTY lifecycle:

**`create(sessionId: string, options: PtyCreateOptions): Promise<PtyCreateResult>`**

Initializes a new PTY session or attaches to an existing tmux session. Returns the initial screen buffer and a session handle.

**`write(sessionId: string, data: string): Promise<void>`**

Transmits raw input (keystrokes) to the remote PTY.

**`resize(sessionId: string, cols: number, rows: number): Promise<void>`**

Adjusts the terminal dimensions to match the UI viewport.

**`read(sessionId: string, onData: (chunk: string) => void): () => void`**

Registers a callback to receive PTY output streams. Returns an unsubscribe function to clean up listeners.

**`destroy(sessionId: string): Promise<void>`**

Terminates the PTY process and releases server-side resources.

These signatures are deliberately platform-agnostic, enabling the `TerminalNode` component in [`src/renderer/nodes/TerminalNode.tsx`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/nodes/TerminalNode.tsx) to operate identically regardless of whether the PTY runs locally or on a remote host.

## Implementing the RemoteTransport Class

To implement a RemoteTransport for nodeterm, create [`src/renderer/terminal/remote-transport.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/terminal/remote-transport.ts) that implements the `TerminalTransport` interface and manages WebSocket communication.

### Establishing the WebSocket Connection

Instantiate a WebSocket connection to your remote server during class construction. The server must expose RPC endpoints equivalent to the Electron main process handlers found in [`src/main/pty-manager.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/main/pty-manager.ts):

```typescript
import { TerminalTransport, PtyCreateOptions, PtyCreateResult } from './transport';

export class RemoteTransport implements TerminalTransport {
  private ws: WebSocket;
  private listeners = new Map<string, (data: string) => void>();

  constructor(private readonly url: string) {
    this.ws = new WebSocket(url);
    this.ws.onmessage = (ev) => this.handleMessage(JSON.parse(ev.data));
  }
}

```

### Mapping Interface Methods to WebSocket Messages

Implement each `TerminalTransport` method to serialize calls as JSON messages:

**Session Creation:**

```typescript
async create(sessionId: string, opts: PtyCreateOptions): Promise<PtyCreateResult> {
  this.ws.send(JSON.stringify({ type: 'pty:create', sessionId, opts }));
  const resp = await this.waitFor(`pty:create:result:${sessionId}`);
  return resp.result as PtyCreateResult;
}

```

**Input Handling:**

```typescript
async write(sessionId: string, data: string): Promise<void> {
  this.ws.send(JSON.stringify({ type: 'pty:write', sessionId, data }));
}

async resize(sessionId: string, cols: number, rows: number): Promise<void> {
  this.ws.send(JSON.stringify({ type: 'pty:resize', sessionId, cols, rows }));
}

```

**Output Streaming:**

```typescript
read(sessionId: string, onData: (chunk: string) => void): () => void {
  this.listeners.set(sessionId, onData);
  return () => this.listeners.delete(sessionId);
}

private handleMessage(msg: any) {
  if (msg.type === 'pty:output' && this.listeners.has(msg.sessionId)) {
    this.listeners.get(msg.sessionId)!(msg.chunk);
  }
}

```

**Cleanup:**

```typescript
async destroy(sessionId: string): Promise<void> {
  this.ws.send(JSON.stringify({ type: 'pty:destroy', sessionId }));
}

```

### Handling Reconnection Logic

Implement resilience logic similar to [`src/main/pty-manager.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/main/pty-manager.ts) by detecting WebSocket disconnects and attempting graceful reconnection. When reconnecting, re-establish any pending `read` subscriptions to ensure continuous output streaming without UI interruption.

## Integrating RemoteTransport into the UI

Modify [`src/renderer/nodes/TerminalNode.tsx`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/nodes/TerminalNode.tsx) to instantiate either `LocalTransport` or `RemoteTransport` based on runtime configuration. Use a `useMemo` hook to persist the transport instance across renders:

```typescript
import { LocalTransport } from '../terminal/local-transport';
import { RemoteTransport } from '../terminal/remote-transport';
import { useSettings } from '../state/settings';

export const TerminalNode = ({ node }) => {
  const settings = useSettings();
  
  const transport = useMemo(() => {
    return settings.transportMode === 'remote'
      ? new RemoteTransport(settings.remoteEndpoint)
      : new LocalTransport();
  }, [settings.transportMode, settings.remoteEndpoint]);
  
  // Transport is now ready for PTY lifecycle management
};

```

This approach allows the `TerminalNode` component to remain transport-agnostic, switching between local and remote PTYs based on a `settings.transportMode` flag or URL query parameters.

## Server-Side WebSocket Support

The server-side implementation requires adapting the existing PTY service to accept WebSocket frames instead of Electron IPC. Create [`src/server/websocket-transport.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/server/websocket-transport.ts) to bridge WebSocket messages to the shared `ptyService`:

```typescript
import { WebSocketServer } from 'ws';
import { ptyService } from './pty-service';

export const startWebSocketTransport = (httpServer: any) => {
  const wss = new WebSocketServer({ server: httpServer });

  wss.on('connection', (ws) => {
    ws.on('message', async (msg) => {
      const data = JSON.parse(msg.toString());
      
      switch (data.type) {
        case 'pty:create':
          const result = await ptyService.create(data.sessionId, data.opts);
          ws.send(JSON.stringify({ 
            type: `pty:create:result:${data.sessionId}`, 
            result 
          }));
          break;
        case 'pty:write':
          await ptyService.write(data.sessionId, data.data);
          break;
        case 'pty:resize':
          await ptyService.resize(data.sessionId, data.cols, data.rows);
          break;
        case 'pty:destroy':
          await ptyService.destroy(data.sessionId);
          break;
      }
    });
  });
};

```

The [`src/server/pty-service.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/server/pty-service.ts) module handles the actual PTY management and can be reused from the Server Edition implementation, requiring only this thin adapter layer to support WebSocket clients.

## Testing Your Implementation

The repository includes [`src/renderer/terminal/transport.test.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/terminal/transport.test.ts) which validates the `LocalTransport` implementation. Create analogous tests for `RemoteTransport` that spin up a mock WebSocket server using the `ws` library or `node:net`:

- Verify that `create()` sends the correct JSON payload and parses the server response
- Confirm that `write()` forwards data without awaiting a response
- Test that `read()` correctly registers callbacks and handles chunked output delivery
- Validate that `destroy()` cleans up both local listeners and remote resources

Ensure your transport implementation satisfies the same unit-test contract as `LocalTransport` to guarantee identical UI behavior across both transport modes.

## Summary

- **The `TerminalTransport` interface** in [`src/renderer/terminal/transport.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/terminal/transport.ts) defines the contract for PTY operations: `create`, `write`, `resize`, `read`, and `destroy`.

- **RemoteTransport implementation** requires creating a WebSocket client in [`src/renderer/terminal/remote-transport.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/terminal/remote-transport.ts) that serializes method calls as JSON messages to a remote server.

- **UI integration** involves modifying [`src/renderer/nodes/TerminalNode.tsx`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/nodes/TerminalNode.tsx) to instantiate the appropriate transport class based on `settings.transportMode`.

- **Server-side adaptation** uses [`src/server/pty-service.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/server/pty-service.ts) with a WebSocket adapter in [`src/server/websocket-transport.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/server/websocket-transport.ts) to handle remote PTY management.

- **Testing** should mirror the existing [`transport.test.ts`](https://github.com/eneskirca/nodeterm/blob/main/transport.test.ts) suite to ensure functional parity between local and remote transports.

## Frequently Asked Questions

### How does RemoteTransport differ from LocalTransport in nodeterm?

**LocalTransport** uses Electron's IPC mechanism via `window.nodeTerminal` to communicate with the main process defined in [`src/main/pty-manager.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/main/pty-manager.ts), spawning PTYs locally using `node-pty`. **RemoteTransport** implements the same `TerminalTransport` interface but routes all calls over WebSocket to a remote server, enabling cloud-hosted terminals or browser-based server editions without local process access.

### What network protocol should I use for RemoteTransport?

WebSocket is the recommended protocol for implementing RemoteTransport, as it provides full-duplex communication necessary for real-time terminal input and output streaming. The protocol uses JSON message frames with types `pty:create`, `pty:write`, `pty:resize`, `pty:destroy`, and `pty:output`, mapped directly to the interface methods defined in [`src/renderer/terminal/transport.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/renderer/terminal/transport.ts).

### How should I handle connection drops in RemoteTransport?

Implement automatic reconnection logic that mirrors the resilience patterns in [`src/main/pty-manager.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/main/pty-manager.ts). Detect WebSocket `onclose` or `onerror` events, implement exponential backoff for reconnection attempts, and re-establish any active `read` subscriptions upon successful reconnection. Store pending writes in a buffer during disconnection to replay once the connection restores.

### Can I reuse existing server components for the remote PTY backend?

Yes, the Server Edition's [`src/server/pty-service.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/server/pty-service.ts) can be reused entirely. You only need to create a thin adapter—such as [`src/server/websocket-transport.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/server/websocket-transport.ts)—that deserializes WebSocket messages and forwards them to the existing service functions. This ensures compatibility with the tmux session management and PTY spawning logic already implemented in the nodeterm codebase.