One browser tab: the project-grouped sidebar on the left, four live shells across three machines in a split-pane workspace, named windows along the bottom, and the workspace / overview / dashboard toggle up top.
A self-hosted control plane for a fleet of developer machines: one web UI, served from a public hub, giving a single operator live terminal access to every machine they own — organized by project, persistent across reconnects, with a mission-control overview of every running terminal at a glance.
Constellate collapses the two axes you juggle today — one terminal tab per project, one per SSH session into each machine — into a single browser tab. Pick a machine or a project, get a live shell; see every running terminal on one overview; reconnect from any device without losing in-flight work. It is a single-operator tool for machines you already own — not a multi-tenant PaaS, not an environment provisioner, not a web IDE.
Module: github.com/rizquuula/Constellate · Go: 1.25+ · License: MIT
See DESIGN.md for the canonical architecture and the full roadmap.
A persistent, live interactive shell in the browser, end to end — plus the mission-control, project, and auth layers on top:
- Agents dial home (outbound only) to the hub over a WebSocket, run yamux over it, and send
Hello+ periodicHeartbeaton a control stream — reconnecting automatically with backoff. - The hub registers agents in a live
machineID → connectionregistry, persists machine, project, session, and audit metadata to SQLite, and brokers a browser WebSocket ↔ a yamux data stream ↔ a PTY on the agent. - The agent spawns a real PTY per session, keeps a bounded scrollback buffer, pipes raw bytes
both ways, and applies resizes. PTYs survive a tab close; re-attaching replays scrollback —
history repaints instantly, then continues live. An agent process restart marks its orphaned
sessions
lost. - Projects group sessions (across machines) into a project-grouped sidebar; sessions may be ungrouped. The terminal UI is a recursive split-pane workspace — many live shells visible at once.
- A mission-control overview renders every live terminal as a colored tile from rate-capped screen snapshots; click a tile to dive into the full interactive session.
- A progress dashboard rolls up fleet/per-machine/per-project status, an attention list, and recent audit events, plus per-session activity badges (active / idle / needs input).
- Auth + TLS: agents enroll with an Ed25519 keypair (the hub stores only the public key); the
operator logs in with TOTP + recovery codes + WebAuthn passkeys; all
/api/*and/ws/*routes are gated by a session cookie; the hub serves HTTPS directly or behind Caddy. - A React + xterm.js app (embedded in the hub binary) serves all of the above as one web UI.
Security note: the hub is a remote-code-execution gateway to every enrolled machine. It is built to face the public internet — over HTTPS, behind operator auth — but treat the deployment accordingly. See
DESIGN.md§10 for the threat model.
Browser ──HTTPS/WSS──► HUB ◄──one TLS WebSocket per agent (outbound dial-home, yamux-muxed)──┐
overview · terminal (public VPS) │
· dashboard • serves the React app + REST/WS ┌────────────┴───────────┐
• brokers browser ↔ agent ↔ PTY Machine 1 Machine 2 … Machine N
• SQLite: machines·projects·sessions·audit AGENT AGENT AGENT
• holds NO shells itself ├ PTYs ├ PTYs ├ PTYs
└ vt └ vt └ vt
Agents dial home (outbound only) — the hub never connects into a machine, so dev boxes need zero
inbound ports and work behind NAT. Each agent holds one TLS WebSocket carrying many yamux
streams (control, per-session data, snapshots). The hub is a pure control plane and relay; PTYs live
on the agents. The codebase is two hexagons in one Go module (internal/hub, internal/agent),
sharing only internal/transport (wire protocol) and internal/platform. See
DESIGN.md §4–§12 for the full design.
- Go 1.25+. If your system Go is older, set
GOTOOLCHAIN=auto(theMakefilealready does) so the toolchain is fetched automatically. - Node 18+ / npm — to build the web app (
make web, run automatically bymake build). - Docker + Compose v2 — only for the Dockerized topology test (
make test-docker).
The fastest way to try it is the one-command Docker demo — it builds the images, bootstraps an operator, enrolls two agent "machines", and prints a login code:
./deploy/dev-up.sh
open http://localhost:8080 # log in with the printed code → pick an agent → "New shell"To install just the agent on a remote machine, use the one-line installer — it downloads the
binary for your OS/arch from the latest release, verifies its SHA-256, and drops it in
/usr/local/bin (override with BIN_DIR=):
curl -fsSL https://raw.githubusercontent.com/rizquuula/Constellate/main/install.sh | sh
# …then enroll + connect. Or enroll in one step by passing the hub + token:
curl -fsSL https://raw.githubusercontent.com/rizquuula/Constellate/main/install.sh \
| CONSTELLATE_HUB=https://your-hub.example CONSTELLATE_TOKEN=<token> shTo run the binaries directly (two terminals), the flow is: build, start the hub, bootstrap an operator, mint an enrollment token, enroll the agent, then run it:
make build # builds both binaries into ./bin
./bin/constellate-hub serve # Terminal 1: serves on 127.0.0.1:8080
./bin/constellate-hub operator add # bootstrap: prints TOTP URI + recovery codes
./bin/constellate-hub enroll-token # mint a one-time agent enrollment token
./bin/constellate-agent enroll --hub http://127.0.0.1:8080 --token <token> # Terminal 2: one-time
./bin/constellate-agent connect # dial home and serveThen open http://127.0.0.1:8080, log in with your 6-digit TOTP code, and the machine appears
online in the sidebar. Click New shell to open a live terminal — type, run ls/top, resize.
Close the tab and re-open it; the shell is still running and its history repaints instantly.
See docs/usage.binary.md and docs/usage.docker.md
for the full walkthroughs (config, TLS, passkeys, multiple machines). Configuration comes from a YAML
file (--config; samples in configs/); per-secret CONSTELLATE_* env vars override
file values.
constellate-hub serve | migrate | version # serve is the default
constellate-agent connect | status | version # connect is the default
Both version commands print the binary version, git commit, and wire protocol version.
make test # unit + integration + in-process E2E (dial-home + the full terminal lifecycle)
make test-e2e # Playwright: a real browser opens a shell on an agent, types, reads, resizes
make test-docker # hub + 2 agent containers on a Docker network — dial-home across real boundaries
make lint # golangci-lintAcceptance checks: online → offline → online (test/integration/topology_test.go, test/docker/)
and the terminal lifecycle — create → attach → type → read → resize → detach → re-attach → close —
both in-process (test/integration/terminal_test.go) and in a real browser (test/e2e/).
CI: the cheap checks (lint, vet, race tests, frontend typecheck/build) run automatically on push/PR
(.github/workflows/ci.yaml). The heavy tiers (Playwright browser + Docker topology) are
manual-trigger only (e2e.yaml, run from the Actions tab) to conserve Actions minutes.
Constellate shows per-session activity (active / idle / awaiting input) in the sidebar,
overview grid, and dashboard. Accuracy improves with optional OSC 133 prompt markers — see
docs/shell-integration.md for setup snippets (bash + zsh).
Two bounded contexts in one module, each its own hexagon (internal/hub, internal/agent), sharing
only internal/transport (the wire protocol) and internal/platform (logging, ids, config, version).
See DESIGN.md §11–§12 for the full layering and folder tree.
Go · coder/websocket ·
hashicorp/yamux · creack/pty ·
modernc.org/sqlite (pure-Go, CGO_ENABLED=0) ·
go-webauthn · pquerna/otp ·
React + xterm.js. The VT/ANSI emulator is an in-repo,
dependency-free pure-Go implementation.
Issues and pull requests are welcome. Before opening a non-trivial PR, please read
DESIGN.md — it is the canonical architecture — and keep changes within the hexagonal
layering it describes. A few house rules:
- Pure Go, static binaries. Keep
CGO_ENABLED=0; don't add cgo dependencies. - Run the gates locally:
make lintandmake testshould pass before you push; the heaviermake test-e2e/make test-dockertiers are worth running for changes to the transport, agent, or browser flow. - Match the surrounding style and keep the two bounded contexts (
internal/hub,internal/agent) from importing each other.
Released under the MIT License. © 2026 M Razif Rizqullah.
