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";importtype{ReactElement}from"react";exportfunctionWorkspaceTerminal(): ReactElement{return(<mainclassName="workspace"><aside>{/* Your existing files, tools, agent timeline, etc. */}</aside><LantermTerminalclassName="terminal"endpoint="ws://localhost:8790/terminal"/></main>);}Server:
Node
import{createServer}from"node:http";import{createPtyEnv,createPtySession,typePtySessionHandle}from"@lanterm/server";import{WebSocket,WebSocketServer}from"ws";constsessions=newWeakMap<WebSocket,PtySessionHandle>();constserver=createServer();constwss=newWebSocketServer({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)=>{constsession=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,typePtySessionHandle}from"@lanterm/server";typeWebSocketData={session?: PtySessionHandle;};Bun.serve<WebSocketData>({fetch(request,server){consturl=newURL(request.url);if(url.pathname==="/terminal"&&server.upgrade(request,{data: {}})){returnundefined;}returnnewResponse("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