---
title: Custom remote transport | Letta Docs
description: Connect a Letta Code remote environment to your own WebSocket endpoint.
---

A custom remote transport lets you run the Remote client protocol without Letta’s hosted environment router. Your service accepts a WebSocket from a Letta Code runtime, sends commands over that socket, and renders the events it receives back.

This is different from App Server: the runtime connects outward to your service rather than accepting a direct connection from an Agent SDK client. For most products, use the [Letta Agent SDK](/agent-sdk/quickstart/index.md) with `backend: "cloud"`. The SDK can manage cloud sandboxes and remote connections without requiring you to build a router. Build a custom transport only when you need to own the routing layer yourself, such as for embedded desktop routing, LAN-only control, private infrastructure, or SDK/runtime development.

## Direct mode

In direct mode, your app is the WebSocket server and the Letta Code runtime connects to it.

```
Letta Code runtime  ── WebSocket ──>  your app
```

```
import { WebSocketServer, WebSocket } from "ws";


const token = process.env.REMOTE_TOKEN ?? "dev-token";
const server = new WebSocketServer({ host: "127.0.0.1", port: 8284 });


let runtime: WebSocket | null = null;


server.on("connection", (socket, request) => {
  if (request.headers.authorization !== `Bearer ${token}`) {
    socket.close(1008, "unauthorized");
    return;
  }


  runtime = socket;


  socket.on("message", (raw) => {
    const event = JSON.parse(raw.toString());
    handleRuntimeEvent(event);
  });


  socket.on("close", () => {
    if (runtime === socket) runtime = null;
  });
});


function send(command: unknown) {
  if (!runtime || runtime.readyState !== WebSocket.OPEN) {
    throw new Error("Remote runtime is not connected");
  }
  runtime.send(JSON.stringify(command));
}
```

Start the runtime with your endpoint:

Terminal window

```
LETTA_BASE_URL=http://localhost:8284 \
IGNORE_SELF_HOSTED_LISTENER_ERROR=1 \
letta server --env-name local-dev --backend local
```

Then send the same v2 frames used by the hosted Remote client API:

```
send({
  type: "input",
  runtime: {
    agent_id: "agent-...",
    conversation_id: "conv-...",
  },
  payload: {
    kind: "create_message",
    messages: [{ role: "user", content: "Inspect this repo." }],
  },
});
```

For shared command semantics, use the [App Server protocol lifecycle](/platform/app-server/protocol-lifecycle/index.md). For hosted transport details, use the [Remote client API reference](/agent-sdk/remote-client/reference/index.md).

## Heartbeats and reconnects

For a single direct socket, keep transport behavior simple:

- send `ping` periodically and expect `pong`
- mark the runtime offline when the socket closes
- send `sync` after reconnecting

You can skip hosted `seq`/`ack` handling in direct mode unless you add replay or multiple subscribers.

## When to add a broker

Add a broker only if direct mode is not enough.

```
Letta Code runtime  <── runtime WS ──>  broker  <── client WS ──>  custom client
```

A broker is useful when you need multiple clients, multiple runtimes, runtime discovery, replay buffers, or internet routing. If you do not need those, direct mode is easier to operate and reason about.

## Security

Bind local development servers to `127.0.0.1`, require a bearer token, and avoid exposing file, terminal, memory, or secret commands to untrusted clients. For LAN or internet deployments, use `wss://`, authenticate both sides, and validate browser origins if browsers can connect.
