Orbit WM is a browser-based tiling window manager for terminals (and local dev browser windows).
It turns any browser (tablet, TV, spare laptop, iPad) into a tiled terminal desktop. You get multiple terminals and dashboards on one screen, layouts that come back after a restart, and a setup that looks and feels like a real window manager without installing a full Linux desktop or giving everyone SSH access.
For solo devs
- You want a Hyprland-style tiling layout, but you’re stuck on macOS, Windows, or iPad.
- You want your terminals, logs, and browsers to reopen exactly how you left them.
- You’re tired of juggling tabs and manually resizing panes every time you start working.
For teams / offices
- You want a TV dashboard that shows logs,
htop/btop, metrics, and status pages. - You want to show live server output to coworkers without giving them SSH access.
- You want a “war room” view everyone can see in the browser during incidents.
For mobile / tablet users
- You have a tablet and want to quickly show multiple terminals on it for development.
- You want your Linux rice (colors, fonts, wallpaper) on an iPad or tablet.
- You hate how clunky most mobile terminal apps feel, especially on iOS.
- You want to plug a tablet into a monitor and get a “fake desktop” made of terminals.
Quality-of-life benefits
- Stop rearranging windows: layouts are saved and restored automatically.
- Don’t lose work: sessions are backed by tmux, so terminals survive browser reloads.
- Use any screen: browser-only, so it works on TVs, thin clients, or borrowed machines.
- Looks good by default: wallpapers, gaps, borders, and terminal theming built in.
- Keyboard-first: fast navigation and swapping without touching the mouse.
It’s designed for “desktop-like” multi-window workflows inside a single web page:
- Multi-terminal dev workflows in a single browser window
- Live dashboards for logs/metrics on a shared screen
- Persistent workspaces that reopen as you left them (layout + session metadata)
- Fast keyboard-driven focus/swap navigation (dwindle-style layout)
- A local “middle layer” that keeps terminal sessions reliable by using tmux
- Node.js (recommended: 20+)
tmuxavailable on your PATHcaddyavailable on your PATH- Build tools for native Node modules (for
node-pty)- macOS: Xcode Command Line Tools
- Linux:
build-essential(or equivalent), plus Python 3
Clone the Orbit repo, then clone the required ghostty-web fork into ./ghostty-web:
git clone <orbit-repo-url>
cd orbit-wm
git clone git@github.com:YuryBecker/ghostty-web.git ghostty-webOrbit depends on a local package reference ("ghostty-web": "file:./ghostty-web"), so this folder must exist before install.
We currently use a fork because Orbit depends on behavior patches in ghostty-web that are not guaranteed in upstream (notably terminal wheel/touch scroll behavior on mobile and browser-hosted terminals).
Then install dependencies:
npm installnpm run orbit:startOrbit prints the LAN URL at startup, for example:
https://192.168.1.50:43123
Internal services are started automatically:
- Next production server on
127.0.0.1:43121 - Middle layer on
127.0.0.1:43120 - Caddy TLS proxy on
0.0.0.0:43123
For hot-reload development, use:
npm run orbit:devFor sandboxed demo mode (Docker-backed shells), use:
npm run orbit-sandbox:image:build
npm run orbit-sandbox:devor production:
npm run orbit-sandbox:startUse this mode to run Orbit as an online demo with isolated shells.
- Runs shell sessions inside Docker instead of host tmux.
- Uses one sandbox container per authenticated user.
- Opens multiple terminal windows as separate PTYs inside that same container.
- Prewarms sandbox capacity on initial page load to reduce first-terminal delay.
- Cleans up idle/expired sessions automatically.
npm run orbit-sandbox:image:build
npm run orbit-sandbox:devnpm run orbit-sandbox:start- Runtime config file:
middle/runtime/docker.config.json - Key controls:
- image/shell/network/resource limits
- readonly filesystem + tmpfs mounts
- prewarm pool size
- auto-build behavior
Orbit has session/device management because the app is powerful enough to open real shell sessions. If someone can reach the URL, you still want control over who actually gets in, and whether they can type or only watch.
- It gives you a safe default for shared LAN use.
- It lets you approve each device intentionally.
- It lets you grant read-only terminal views for dashboards or demos.
- Admin session (local machine): when you open Orbit on
127.0.0.1, you’re treated as the local admin. - Guest session (phone/tablet/computer): when a new device opens the LAN URL, it creates a pending request and waits.
- Approval flow: the admin gets a toast notification and can open Devices → Manage Devices to allow or deny.
- Permissions: approved users can be
control(can type/resize/create) orreadonly(view-only).
- In the main menu, use Devices → Add Device.
- Orbit opens a pairing dialog with:
- a QR code to scan on mobile
- a copyable link for sharing manually
- You can generate either Control or Readonly pairing links directly from that dialog.
In Devices → Manage Devices, you can:
- review current pending requests
- approve or deny requests
- view known users
- toggle a user between control and readonly
- revoke tokens if someone should no longer have access
- review request history in case someone was denied by mistake
When you create a terminal window:
- The frontend
POSTsmiddleatPOST /api/session - The middle layer creates a tmux session (server name defaults to
orbit, sessions are namedorbit-<sessionId>) - The middle layer spawns a PTY attached to tmux and exposes it over Socket.IO at the
/terminalnamespace - The browser connects to Socket.IO with
{ auth: { sessionId } }and streams input/output
- SQLite:
middle/db.sqlitestores session metadata (sessionstable) and UI config (configtable) - tmux: holds the actual terminal processes
- Browser localStorage: stores the last opened layout id as
orbitLayoutId(legacy fallback:orbitSessionId)
If the middle layer restarts, it can restore sessions from SQLite if the tmux session still exists.
Caddy is the HTTPS front door for Orbit.
Orbit has two internal HTTP services running on loopback:
- frontend (
127.0.0.1:43121) - middle API + Socket.IO (
127.0.0.1:43120)
Caddy listens on https://<your-ip>:43123, handles TLS (tls internal), and forwards traffic:
/api/*and/socket.io/*-> middle layer- everything else -> frontend
http://<your-ip>:80-> permanent redirect tohttps://<your-ip>:43123
So from your phone/tablet you only use one URL, and the browser sees a single secure origin for UI + API + WebSockets.
Orbit WM uses a modifier key (currently defaults to Ctrl).
Ctrl+Enter: New terminal windowCtrl+Arrow keysorCtrl+h/j/k/l: Focus neighbor (left/down/up/right)Ctrl+Shift+Arrow keysorCtrl+Shift+H/J/K/L: Swap with neighborCtrl+q: Close active window
Browser windows:
Ctrl+q/Cmd+qinside the iframe attempts to close that browser window- Holding
Ctrltemporarily disables iframe pointer events (useful for dragging windows over embedded pages)
Drag a window by its title bar and drop it onto another window to swap their positions.
Menu → Wallpaper / Terminal / Layout:
- Switch between built-in wallpapers or upload a custom wallpaper
- Adjust terminal padding, opacity, color, and font
- Adjust layout gap, borders, and shadows
Set these in .env.local (optional):
NEXT_PUBLIC_MIDDLE_URL: Full URL for the middle layer (example:http://localhost:4001)NEXT_PUBLIC_MIDDLE_PORT: Port used to construct the middle layer URL whenNEXT_PUBLIC_MIDDLE_URLis not set (default:4001)
Provide these when starting npm run middle:dev or npm run middle:start:
MIDDLE_PORT: HTTP port for the middle layer (default:4001)MIDDLE_HOST: bind host for middle layer (default:127.0.0.1)CLIENT_ORIGIN: Comma-separated allowlist for CORS origins (example:http://localhost:43123)ORBIT_RUNTIME: Session runtime backend (hostordocker, default:host)ORBIT_ACCESS_MODE: Access policy (approvalorauto). Defaults toapprovalforhostruntime andautofordockerruntime.- In
automode (demo), custom wallpaper file uploads are disabled server-side and custom images are kept in browser localStorage only.
- In
ORBIT_TMUX_BIN: Path totmux(default:tmux)ORBIT_TMUX_SERVER: tmux server name passed totmux -L(default:orbit)ORBIT_TERMINAL_REPLAY_BYTES: Max bytes of terminal output to replay to newly connected clients (default:200000)ORBIT_DEBUG_PTY_OUTPUT: Set to1to log PTY output on the server
When ORBIT_RUNTIME=docker, runtime defaults come from:
middle/runtime/docker.config.json
You can override the config path with:
ORBIT_DOCKER_CONFIG_FILE
When ORBIT_RUNTIME=docker, optional runtime variables:
ORBIT_DOCKER_BIN(default:docker)ORBIT_DOCKER_IMAGE(default:orbit-sandbox-shell:latest)ORBIT_DOCKER_SHELL(default:/usr/bin/fish)ORBIT_DOCKER_NETWORK(default:none)ORBIT_DOCKER_MEMORY(default:512m)ORBIT_DOCKER_CPUS(default:1)ORBIT_DOCKER_PIDS_LIMIT(default:128)ORBIT_DOCKER_USER(default:1000:1000)ORBIT_DOCKER_READ_ONLY(default:true)ORBIT_DOCKER_CAP_DROP_ALL(default:true)ORBIT_DOCKER_NO_NEW_PRIVILEGES(default:true)ORBIT_DOCKER_TMPFS(comma-separated list)ORBIT_DOCKER_EXTRA_RUN_ARGS(comma-separated list)ORBIT_DOCKER_PREWARM_POOL_SIZE(default:1)ORBIT_DOCKER_AUTO_BUILD(default fororbit-sandbox:*:1)ORBIT_DOCKER_BUILD_CONTEXT(default:docker/sandbox)ORBIT_DOCKER_BUILD_FILE(default:docker/sandbox/Dockerfile)ORBIT_SANDBOX_IDLE_TIMEOUT_MS(default:900000)ORBIT_SANDBOX_TTL_MS(default:3600000)ORBIT_SANDBOX_CLEANUP_INTERVAL_MS(default:30000)ORBIT_SANDBOX_ACTIVITY_DEBOUNCE_MS(default:5000)
Runtime prewarm endpoint:
POST /api/runtime/prewarm(control scope required)- In Docker runtime, sessions are grouped per authenticated user so multiple terminal windows share one sandbox container.
npm run orbit:start and npm run orbit:dev support:
ORBIT_HTTPS_PORT(default:43123)ORBIT_HTTP_PORT(default:8080)ORBIT_MIDDLE_PORT(default:43120)ORBIT_NEXT_PORT(default:43121)
Example:
ORBIT_HTTPS_PORT=43123 ORBIT_HTTP_PORT=8080 ORBIT_MIDDLE_PORT=43120 ORBIT_NEXT_PORT=43121 npm run orbit:start- “tmux: command not found”: install tmux or set
ORBIT_TMUX_BINto the full path. caddy: command not found: install Caddy and ensurecaddyis on your PATH.CERT_AUTHORITY_INVALIDon phone/tablet: expected withtls internaluntil the device trusts Caddy's local CA at.orbit/xdg-data/caddy/pki/authorities/local/root.crt.- CORS errors / blocked requests: set
CLIENT_ORIGIN=http://localhost:43123(or your actual frontend origin). node-ptyinstall/build failures: install platform build tools (see prerequisites).- Stale sessions / weird state: stop servers, kill the tmux server (
tmux -L orbit kill-server), and removemiddle/db.sqlite(this resets persisted sessions/config).



