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

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, 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 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.

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) treats a session as the primary logical unit of OpenCode, storing conversation history, code edits, and references to active PTY instances.

Session CRUD Operations

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) 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

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:

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) 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.

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 provides type-safe methods for all server interactions:

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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →