---
title: Self hosted remote API (BYOR) | Letta Docs
description: Self-host the remote API by connecting a custom client directly to a Letta Code runtime without Letta Cloud.
---

You can use the Remote Client API without Letta Cloud. In a self-hosted remote, your app owns the transport between a Letta Code runtime and your custom client.

## Quickstart

### 1. Start your client WebSocket server

Your custom client can be a WebSocket server. It accepts one Letta Code connection, renders events, and sends commands back over the same socket.

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


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


let lettaCodeSocket: WebSocket | null = null;


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


  lettaCodeSocket = socket;


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


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


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


function handleRuntimeEvent(event: any) {
  if (event.type === "stream_delta") {
    console.log(event.delta);
  }
  if (event.type === "control_request") {
    console.log("approval required", event);
  }
}
```

### 2. Start `letta remote` with your endpoint

Start the stock Letta Code remote listener with `LETTA_BASE_URL` pointing at your self-hosted server:

Terminal window

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

`LETTA_BASE_URL` is the custom server endpoint.

For deeper command and event details, see the [API reference](/letta-code/remote-client-api-reference/index.md).

### 3. Send a message

After the runtime connects, send an `input` command over the socket:

```
sendCommand({
  type: "input",
  runtime: {
    agent_id: "agent-...",
    conversation_id: "conv-...",
  },
  payload: {
    kind: "create_message",
    messages: [
      {
        role: "user",
        content: "What files should I inspect first?",
        client_message_id: "cm_01J_demo",
      },
    ],
    supports_control_response: true,
  },
});
```

### 4. Render streamed events

Handle runtime events from the same socket:

```
function handleRuntimeEvent(event: any) {
  switch (event.type) {
    case "update_device_status":
      updateDeviceStatus(event.device_status);
      break;
    case "update_loop_status":
      updateRunStatus(event.loop_status);
      break;
    case "stream_delta":
      appendStreamDelta(event.delta);
      break;
    case "control_request":
      showApprovalPrompt(event);
      break;
    default:
      handleResponseOrOtherEvent(event);
  }
}
```

## Heartbeats and reconnects

For direct mode, keep heartbeats simple:

- Client sends `{ "type": "ping" }` every 30 seconds.
- Runtime replies with `{ "type": "pong", "timestamp": Date.now() }`.
- If the socket closes, mark the runtime offline.
- When the runtime reconnects, send `sync` to replay current state.

You can skip transport `seq` and `ack` in a single-socket direct transport. Add them only if you need replay, gap detection, or compatibility with clients that expect hosted remote semantics.

## When to add a broker

Add a broker/router only when direct runtime-to-client is not enough.

Use a broker if you need:

- Multiple clients watching the same runtime.
- Multiple Letta Code runtimes behind one endpoint.
- Runtime selection or discovery.
- Per-subscriber `seq` stamping and ACK tracking.
- Reconnect parking while the runtime restarts.
- Internet routing when the client and runtime cannot reach each other directly.
- Optional proxying to Letta Cloud for some connections.

A broker has the same shape as Letta Code Desktop’s local environment server:

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

But it is not required for BYOR.

## Security checklist

For local BYOR:

- Bind to `127.0.0.1` unless you intentionally need LAN access.
- Require a per-session token on the WebSocket.
- Prefer `Authorization: Bearer <token>` when the runtime/client can set headers.
- Validate browser `Origin` headers if a browser can connect.
- Do not expose file, terminal, memory, or secret commands to untrusted clients.

For LAN or internet BYOR:

- Use TLS (`wss://`).
- Authenticate both sides.
- Restrict which origins, IPs, or users can connect.
- Treat file, terminal, memory, and secret commands as privileged.

## When to use Letta Cloud instead

Use Letta Cloud remote environments when you need:

- Remote access over the internet without custom networking.
- Hosted authentication and remote environment selection.
- Multiple clients from different networks.
- Durable remote environment records.
- Less custom infrastructure to maintain.

Use BYOR when you want direct local control, embedded desktop routing, LAN-only remotes, or a private command center inside your own application.
