Reference

Server and API reference

Environment variables, authentication, HTTP endpoints, and device storage.

Node.js + TypeScript control plane that relays terminal sessions between browsers and agents.

Configuration

The server loads the nearest .env file automatically (searching upward from its working directory), so a repo-root .env works whether you start from the root or from server/. Values already set in the environment take precedence over the file.

VariableDefaultDescription
ADMIN_PASSWORD(none)Required. Web UI password, min 12 chars in production. Setting it turns authentication on; the server refuses to start without it unless SPECTRE_DEV_NO_AUTH=1
SPECTRE_DEV_NO_AUTHSet to 1 to let the server start with no password (auth off) for local development. Ignored when ADMIN_PASSWORD is set — a password always means auth is on. Fatal when NODE_ENV=production
SPECTRE_PUBLIC_HOST(auto)Address agents should dial, shown in the UI's enrollment command, e.g. wss://spectre.example.com. When unset and TRUST_PROXY=1, the UI advertises the origin the browser reached the proxy on; otherwise the server's detected LAN address and PORT
PORT8080HTTP/API port. Everything is served under /api
DATA_DIR./dataSQLite database location (spectre.db, written 0600)
CORS_ORIGIN(empty)Comma-separated allowed origins. Empty = no cross-origin access
TRUST_PROXYSet to 1 only behind a proxy that sets X-Forwarded-For and X-Forwarded-Proto. The supplied proxy container does both
SPECTRE_DEBUG_TERMINALSet to 1 to log terminal output summaries. Off by default: output contains what the user typed

Authentication is on exactly when ADMIN_PASSWORD is set, so its state is consistent across page loads. SPECTRE_DEV_NO_AUTH only permits running without a password; it never overrides one.

Authentication

  • Browser → server: POST /api/auth/login with the password returns a session token, sent as Authorization: Bearer <token>. Sessions idle out after 24h and expire absolutely after 7 days. Repeated failed logins lock out the client.
  • Browser → WebSocket: browsers can't set headers on a WebSocket handshake, so the session is exchanged via POST /api/auth/ws-ticket for a single-use ticket valid for 30 seconds, passed as ?ticket=. Session tokens are never accepted in a URL.
  • Agent → server: Authorization: Bearer <device key | auth key> on the handshake. Agents are authenticated before the WebSocket is established.

HTTP API

Public:

EndpointDescription
GET /api/healthzLiveness probe
GET /api/versionServer version
GET /api/auth/status{ authEnabled: true/false }
POST /api/auth/login{ "password": "..." }{ "token": "..." }. Rate limited
POST /api/devices/approval-requestAgent asks to be approved → { userCode, pollToken, expiresAt }. Rate limited
POST /api/devices/approval-pollAgent polls → { status: "pending" | "approved" | "expired", deviceKey? }. Rate limited

Requires Authorization: Bearer <session token>:

EndpointDescription
POST /api/auth/logoutInvalidate the current session
POST /api/auth/ws-ticket{ ticket } for a WebSocket upgrade
GET /api/agentsList agents with status, system info, Docker containers
POST /api/agents/:id/commandPush keystrokes. { "data": "ls\n" }
POST /api/agents/refresh-docker | -system | -networkRe-fetch info from all agents
POST /api/agents/:id/updateAsk a connected machine to upgrade itself. { version? }, defaulting to the latest release. 409 when the machine is offline
POST /api/authkeysCreate an auth key. { reusable?, expiresInMs?, description? }{ key, ... }. The plaintext key is returned only here
GET /api/authkeysList auth keys (hints only, never the key)
DELETE /api/authkeys/:idRevoke an auth key
GET /api/devicesList enrolled devices (one per physical machine). Never includes key material
GET /api/devices/:id/connectionsConnection history for a device
DELETE /api/devices/:idRevoke a device and drop its live connections
GET /api/devices/pendingMachines waiting for approval
POST /api/devices/pending/:userCode/approveApprove a machine. { name? }
POST /api/devices/pending/:userCode/denyDeny and discard the request

WebSocket:

PathAuthDescription
WS /api/terminal?id=<agentId>&ticket=<t>Single-use ticketBrowser terminal I/O
WS /api/agents/events?ticket=<t>Single-use ticketLive agent status stream
WS /api/agents/registerAuthorization: Bearer <device key | auth key>Agent registration

Device store

State lives in a SQLite database (spectre.db in DATA_DIR, via Node's built-in node:sqlite, written 0600): enrolled devices and their last-known state, auth keys, pending approvals, and a connection history. All credentials are stored as SHA-256 hashes — reading the file is not enough to impersonate a device or enrol a new one. These are high-entropy random tokens rather than passwords, so a fast hash is appropriate; there is nothing to brute-force. A pre-existing store.json from an earlier version is imported once on first start and renamed to store.json.imported.

Device identity. A device is keyed by a stable hardware identity derived from what the agent reports — the Linux machine-id if present, otherwise its set of MAC addresses, otherwise the agent's persistent device id. This is why a machine that disconnects and reconnects — or is re-enrolled with a new key — stays a single row that flips between connected and disconnected, rather than appearing twice.

On this page