Not another web terminal server. Lanterm is a library toolkit for building your own PTY-backed terminal UX.
Unlike web terminal servers such as ttyd, Lanterm is designed for teams that want to:
- embedding real PTY-backed terminals without adopting a full terminal server
- control session lifecycle, transport, and UI from their own application
- build shell workspaces for coding agents, browser IDEs, ops tools, and dashboards
@lanterm/protocol: shared message types and codecs@lanterm/server: server-side PTY and WebSocket utilities@lanterm/pty: native PTY binding used by@lanterm/server
@lanterm/react: xterm-backed React surface
Lanterm fills the gap between full web terminal servers and low-level terminal building blocks, letting you easily add a real PTY-backed terminal to your own UI with a regular React component.
Client:
React
import "@xterm/xterm/css/xterm.css";
import { LantermTerminal } from "@lanterm/react";
import type { ReactElement } from "react";
export function WorkspaceTerminal(): ReactElement {
return (
<main className="workspace">
<aside>{/* Your existing files, tools, agent timeline, etc. */}</aside>
<LantermTerminal
className="terminal"
endpoint="ws://localhost:8790/terminal"
/>
</main>
);
}Server:
Node
import { createServer } from "node:http";
import { createPtyEnv, createPtySession, type PtySessionHandle } from "@lanterm/server";
import { WebSocket, WebSocketServer } from "ws";
const sessions = new WeakMap<WebSocket, PtySessionHandle>();
const server = createServer();
const wss = new WebSocketServer({ noServer: true });
server.on("upgrade", (request, socket, head) => {
if (request.url !== "/terminal") {
socket.destroy();
return;
}
wss.handleUpgrade(request, socket, head, (websocket) => {
wss.emit("connection", websocket, request);
});
});
wss.on("connection", (socket) => {
const session = createPtySession(socket, {
args: [],
cwd: process.cwd(),
env: createPtyEnv(process.env),
shell: process.env.SHELL ?? "/bin/sh"
});
sessions.set(socket, session);
socket.on("message", (data) => {
session.handle(data.toString());
});
socket.on("close", () => {
sessions.delete(socket);
session.close();
});
});
server.listen(8790, "127.0.0.1", () => {
console.log("Lanterm Node backend listening on ws://127.0.0.1:8790/terminal");
});Bun
import { createPtyEnv, createPtySession, type PtySessionHandle } from "@lanterm/server";
type WebSocketData = {
session?: PtySessionHandle;
};
Bun.serve<WebSocketData>({
fetch(request, server) {
const url = new URL(request.url);
if (url.pathname === "/terminal" && server.upgrade(request, { data: {} })) {
return undefined;
}
return new Response("Lanterm Bun backend", { status: 200 });
},
hostname: "127.0.0.1",
port: 8790,
websocket: {
close(socket) {
socket.data.session?.close();
socket.data.session = undefined;
},
message(socket, data) {
socket.data.session?.handle(data);
},
open(socket) {
socket.data.session = createPtySession(socket, {
args: [],
cwd: process.cwd(),
env: createPtyEnv(process.env),
shell: process.env.SHELL ?? "/bin/sh"
});
}
}
});The React client source is shared from examples/client; each backend example owns its Vite setup.
Run the shared client with the Bun backend:
bun install --cwd examples/bun
bun run --cwd examples/bun devRun the shared client with the Node backend:
pnpm --dir examples/node install
pnpm --dir examples/node dev