# OpenCode Server Architecture: A Deep Dive into PTY, TUI, and Session Routes

> Explore OpenCodes server architecture. Learn how Hono, Session routes, PTY routes, and TUI routes manage terminal UI and pseudo-terminal lifecycles for seamless collaboration.

- Repository: [Anomaly/opencode](https://github.com/anomalyco/opencode)
- Tags: architecture
- Published: 2026-02-16

---

**OpenCode uses a modular Hono-based HTTP server where Session routes manage conversation state, PTY routes handle pseudo-terminal lifecycles over WebSockets, and TUI routes bridge browser-based terminal UI events to the underlying PTY layer.**

OpenCode (from the `anomalyco/opencode` repository) implements a single Node-based HTTP service built on **[Hono](https://github.com/honojs/hono)**, a lightweight router designed for edge environments. The server architecture separates concerns into lazy-loaded sub-routers, allowing each feature—sessions, pseudo-terminals (PTY), and the Terminal User Interface (TUI)—to operate independently while sharing a unified request context.

## Core Server Foundation

The server entry point in [`packages/opencode/src/server/server.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/server.ts) instantiates a root Hono application and mounts feature-specific routers using lazy loading. This pattern ensures that route handlers are only imported when first requested, reducing cold-start latency.

```typescript
import { Hono } from "hono"
import { lazy } from "hono/lazy"
import { SessionRoutes } from "./routes/session"
import { PtyRoutes } from "./routes/pty"
import { TuiRoutes } from "./routes/tui"

const app = new Hono()

app.route("/session", lazy(() => SessionRoutes()))
app.route("/pty", lazy(() => PtyRoutes()))
app.route("/tui", lazy(() => TuiRoutes()))

export default app

```

Each router is an isolated Hono instance with its own middleware chain, preventing cross-contamination between the session management API and the real-time terminal streams.

## Session Routes

The Session router ([`packages/opencode/src/server/routes/session.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/routes/session.ts)) treats a **session** as the primary logical unit of OpenCode, storing conversation history, code edits, and references to active PTY instances.

### Session CRUD Operations

```typescript
const router = new Hono()

router.get("/", async (ctx) => {
  // List all sessions for the authenticated user
})

router.post("/", async (ctx) => {
  // Create new session with title, model, and timestamps
})

router.get("/:id", async (ctx) => {
  // Retrieve specific session metadata
})

router.patch("/:id", async (ctx) => {
  // Update session properties (title, archived status)
})

router.delete("/:id", async (ctx) => {
  // Remove session and cascade-delete associated PTYs
})

export const SessionRoutes = lazy(() => router)

```

Sessions maintain a foreign-key relationship to PTY instances, ensuring that when a user reconnects to a session, any previously spawned terminal can be reattached or cleaned up gracefully.

## PTY (Pseudo-Terminal) Architecture

The PTY router ([`packages/opencode/src/server/routes/pty.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/routes/pty.ts)) manages the lifecycle of operating-system pseudo-terminals using the `node-pty` library. It exposes both REST endpoints for management and a WebSocket endpoint for real-time I/O.

### PTY Lifecycle Management

```typescript
const router = new Hono()

router.get("/", async (ctx) => {
  // List active PTY instances with metadata (PID, sessionId, creation time)
})

router.post("/", async (ctx) => {
  const { sessionId, cols, rows } = await ctx.req.json()
  const pty = await createPty({ sessionId, cols, rows })
  return ctx.json({ id: pty.id, pid: pty.pid })
})

router.get("/:ptyId", async (ctx) => {
  // Get details of specific PTY
})

router.patch("/:ptyId", async (ctx) => {
  // Resize PTY dimensions (cols/rows)
  const { cols, rows } = await ctx.req.json()
  await resizePty(ctx.req.param("ptyId"), cols, rows)
})

router.delete("/:ptyId", async (ctx) => {
  // Terminate PTY process and cleanup
  await killPty(ctx.req.param("ptyId"))
})

```

### Real-Time WebSocket Streaming

The PTY router upgrades HTTP connections to WebSockets for bidirectional binary streaming:

```typescript
router.get("/:ptyId/ws", async (ctx) => {
  const { ptyId } = ctx.req.param()
  
  // Cloudflare Workers compatible WebSocket upgrade
  const ws = ctx.env.WS.upgrade(ctx.req.raw)
  const pty = await getPtyById(ptyId)
  
  // Pipe PTY stdout → WebSocket
  pty.onData((data: string) => {
    ws.send(data)
  })
  
  // Pipe WebSocket → PTY stdin
  ws.addEventListener("message", (event) => {
    writePty(pty, event.data)
  })
  
  // Cleanup on disconnect
  ws.addEventListener("close", () => {
    pty.kill()
  })
})

```

This architecture decouples the terminal emulation from the transport layer, allowing the PTY to run as a native OS process while the browser interacts via standard WebSocket APIs.

## TUI (Terminal User Interface) Routes

The TUI router ([`packages/opencode/src/server/routes/tui.ts`](https://github.com/anomalyco/opencode/blob/main/packages/opencode/src/server/routes/tui.ts)) serves the browser-based terminal interface. Unlike the PTY router, it does not manage process lifecycles directly; instead, it acts as a bridge between the user's browser and the PTY layer.

```typescript
const router = new Hono()

router.get("/ws", async (ctx) => {
  // Upgrade to WebSocket for UI events
  const ws = ctx.env.WS.upgrade(ctx.req.raw)
  
  // On connection, create or reattach to a PTY for the current session
  const sessionId = ctx.get("sessionId")
  const pty = await getOrCreatePty(sessionId)
  
  // Forward UI events (keystrokes, mouse) to PTY
  ws.addEventListener("message", (event) => {
    const uiEvent = JSON.parse(event.data)
    forwardToPty(pty.id, uiEvent)
  })
  
  // Stream PTY output back to UI
  pty.onData(data => ws.send(data))
})

```

This separation of concerns ensures that the TUI remains a pure presentation layer, while all terminal state and process management resides in the PTY router.

## How the Components Interact

The data flow through OpenCode's server architecture follows a strict hierarchy:

1. **Session Creation**: Client calls `POST /session` → Session router creates a logical container with a unique ID.
2. **TUI Initialization**: Browser opens WebSocket to `/tui/ws` → TUI router validates the session and prepares the UI channel.
3. **PTY Allocation**: TUI router (or client via SDK) calls `POST /pty` with the session ID → PTY router spawns a `node-pty` process and returns a PTY ID.
4. **Real-Time I/O**: Browser opens second WebSocket to `/pty/{id}/ws` → Bidirectional stream established between browser and OS process.
5. **Cleanup**: On disconnect, TUI router signals PTY router to `DELETE /pty/{id}`, terminating the process and updating the session record.

## SDK Integration

OpenCode auto-generates a TypeScript SDK that mirrors the Hono routes exactly. The generated client in [`packages/sdk/js/src/v2/gen/sdk.gen.ts`](https://github.com/anomalyco/opencode/blob/main/packages/sdk/js/src/v2/gen/sdk.gen.ts) provides type-safe methods for all server interactions:

```typescript
import { createClient } from "@opencode/sdk"

const client = createClient({ baseUrl: "https://api.opencode.dev" })

// Session management
const { data: session } = await client.session.create({
  title: "Feature implementation",
  model: "openai/gpt-4o-mini"
})

// PTY lifecycle
const { data: pty } = await client.pty.create({
  sessionId: session.id,
  cols: 100,
  rows: 30
})

// Real-time connection (native WebSocket)
const socket = new WebSocket(`wss://api.opencode.dev/pty/${pty.id}/ws`)
socket.addEventListener("message", (ev) => console.log(ev.data))

```

This generated SDK ensures that any changes to the Hono routes are immediately reflected in the client-side TypeScript definitions, maintaining a single source of truth across the stack.

## Summary

- **OpenCode server architecture** relies on a single Hono-based HTTP service that uses lazy-loaded routers to separate concerns.
- **Session routes** (`/session`) manage the logical containers for conversations and code edits, providing full CRUD operations.
- **PTY routes** (`/pty`) handle pseudo-terminal lifecycles using `node-pty`, exposing both REST endpoints for management and WebSocket endpoints for real-time I/O.
- **TUI routes** (`/tui`) serve as a WebSocket bridge between the browser-based terminal UI and the underlying PTY layer, maintaining strict separation between presentation and process management.
- **Auto-generated SDK** in [`packages/sdk/js/src/v2/gen/sdk.gen.ts`](https://github.com/anomalyco/opencode/blob/main/packages/sdk/js/src/v2/gen/sdk.gen.ts) provides type-safe client methods that mirror the server routes exactly.

## Frequently Asked Questions

### How does OpenCode handle WebSocket upgrades for PTY connections?

OpenCode uses a Cloudflare Workers-compatible WebSocket upgrade mechanism via `ctx.env.WS.upgrade(ctx.req.raw)` inside the `/pty/:ptyId/ws` route handler. This upgrades the HTTP connection to a WebSocket, allowing bidirectional streaming between the browser and the `node-pty` process. The server then pipes PTY stdout to the WebSocket and writes incoming WebSocket messages directly to the PTY stdin.

### What is the relationship between Sessions and PTYs in OpenCode?

Sessions act as the logical container for user activity, storing conversation history, code edits, and metadata. PTYs (pseudo-terminals) are child processes tied to a specific session ID. When a user creates a session via `POST /session`, they receive a session ID that must be passed when creating a PTY via `POST /pty`. This linkage ensures that terminal state persists within the context of a specific conversation and can be cleaned up when the session is deleted.

### Why does OpenCode separate TUI routes from PTY routes?

The separation enforces a clean architecture where the TUI router (`/tui`) handles only presentation-layer concerns—accepting UI events like keystrokes and mouse clicks from the browser—while the PTY router (`/pty`) manages the actual operating system processes via `node-pty`. The TUI WebSocket acts as a thin bridge, forwarding user input to the PTY route and streaming output back to the browser. This decoupling allows the terminal emulator (TUI) to evolve independently from the process management logic (PTY).

### How does the auto-generated SDK maintain compatibility with server routes?

OpenCode generates TypeScript SDK code in [`packages/sdk/js/src/v2/gen/sdk.gen.ts`](https://github.com/anomalyco/opencode/blob/main/packages/sdk/js/src/v2/gen/sdk.gen.ts) that mirrors the Hono router definitions exactly. When the server routes change, the SDK is regenerated to reflect new endpoints, HTTP methods, and request/response schemas. This creates a single source of truth where `client.session.create()` maps directly to `POST /session` and `client.pty.create()` maps to `POST /pty`, ensuring type safety and eliminating manual client-server synchronization errors.