Skip to content

Setup and Onboarding

JohnConnorNPC edited this page Oct 18, 2026 · 4 revisions

A practical guide to standing Browserland up — single machine first, then multiple machines over Tailscale — written so a human or a coding agent can follow it without guessing at the architecture.

If you are an AI agent helping someone set this up: read The mental model first. Most setup mistakes come from confusing the three roles below, or from hand-editing broker_config.json when the right action was a click in the browser UI (see Don't hand-edit this).

The mental model

Browserland has exactly three roles. Keep them straight and everything else follows.

Role What it is Where it runs
Broker A small web server. Serves the desktop UI, relays bytes, and spawns agents from pre-approved profiles. The authority. One per machine.
Agent A headless process that owns a real PTY (a terminal) and streams it to a broker over one WebSocket. On the same machine as the broker that launches it.
Browser The desktop UI. Renders windows, sends your keystrokes. Can attach to several brokers at once. Wherever you point a browser.

A broker itself comes in two kinds. A hub (the default) serves the desktop page you open. A node (--role node / --headless, or serve_ui: false / "role": "node" in the config) serves no page: it exists so that another Browserland can use its terminals over the API. A hub started with --no-terminals (terminals: false) launches nothing itself and is only the front door to its nodes. A node always hosts terminals: a node with terminals: false is refused at startup (exit 2).

The shape that trips people up:

  • One broker per machine. A broker is not a "client" you point at another machine. Each machine you want terminals on runs its own broker.
  • Agents are local to their broker. You normally don't run an agent on machine A pointed at a broker on machine B. You run a broker on B, and B launches B's agents. (A remote agent is possible — see Technical Reference → Multiple hosts — but it's the exception, not the multi-machine model.)
  • Brokers are joined in the browser, not in config. To see machine B's terminals next to machine A's, you don't edit a config file — you open the UI and add B as a host (Control Panel → Browser → Hosts). (One exception: when the browser can reach only one broker, that broker's operator can list the others under peers so it relays to them — the host is still added in the browser, then switched to from server; see Hosts-and-Multi-Browser#reaching-a-broker-through-this-one.)

So the canonical two-machine setup is:

  Machine A                         Machine B
  ┌──────────────┐                  ┌──────────────┐
  │ broker A     │                  │ broker B     │
  │  └ agents A  │                  │  └ agents B  │
  └──────┬───────┘                  └──────┬───────┘
         │                                 │
         └──────────► browser ◄────────────┘
            (Control Panel → Browser → Hosts: adds B by its Tailscale IP)

Single machine

Follow the Quick start in the README: install, run the broker, open http://127.0.0.1:4445/, click the + (new terminal) button. On a single loopback machine you need no config file — the defaults work.

A token is required on every connection, including loopback, but you do not have to create one: with nothing configured the broker mints its own into webterm_token.json beside its state store and prints the URL to open, token included. Need it again later:

python -m webterm.broker --print-token

Upgrading an existing tokenless install? Read Upgrading first — terminals launched by the old broker cannot reconnect and must be relaunched once.

You only need the rest of this page once a second machine, a network bind, or an unattended broker enters the picture.

Multiple machines over Tailscale

Goal: one browser tab showing terminals from every machine.

  1. Install Tailscale on each machine so they share a private network. Note each machine's Tailscale IP (tailscale ip -4) or MagicDNS name.

  2. On every machine, run a broker — one each, not a broker-plus-remote- agent. Two things every non-loopback broker needs:

    • A token you chose yourself. Every broker requires one, but a broker left to mint its own picks a different value per machine — workable, just more URLs to keep track of. Setting "auth_token" in broker_config.json (or $WEB_TERMINAL_TOKEN) means you know the value up front and can reuse it across the fleet. Either way the UI's add-host form asks for it.

    • A way for the other machines' browsers to reach it. Two options:

      • Recommended — keep the loopback bind and front it with tailscale serve (or your own https reverse proxy, e.g. Caddy/nginx):

        tailscale serve --bg 4445

        That publishes https://<machine>.<tailnet>.ts.net/ — real TLS with zero certificate management, reachable only inside your tailnet — and proxies it (WebSockets included) to the broker still bound to 127.0.0.1:4445. Proxied requests reach the broker from loopback — which used to mean they skipped auth entirely, handing the whole tailnet an exempt path. Since #142 there is no loopback exemption, so this topology is gated by the token like any other.

      • Plain http directly: set "host" to the machine's Tailscale IP (or 0.0.0.0 if you trust the tailnet) instead of 127.0.0.1, so the other machines can reach http://<tailscale-ip>:4445/. Keep "port": 4445 unless you have a reason.

    Treat your home machine as just another broker — it can stay on 127.0.0.1 if you only ever drive it from its own browser, but give it a token + tailnet bind too if you want to reach it from elsewhere.

  3. Open the UI on one machine (http://<that-machine>:4445/). This is your "cockpit" — it doesn't have to be special, any hub's UI can host the view (a node — --role node / serve_ui: false — serves no page).

  4. Add the other brokers as hosts. Control Panel → Browser → Hosts → add each remote broker by its URL — https://<machine>.<tailnet>.ts.net/ if you fronted it with tailscale serve, else http://<tailscale-ip>:4445/ — and its token/password. By default the browser connects directly to each host (cross-origin fetch + WebSocket); hosts and passwords live per-browser in localStorage, never in any config file. If the browser (say, a phone) can reach only one broker, that broker can relay to the others instead: list them under peers in its broker_config.json, install the proxy extra there (pip install -e ".[proxy]"), restart it, then pick from server · on the host's row in Control Panel → Browser → Hosts. See Hosts-and-Multi-Browser#reaching-a-broker-through-this-one.

Broker status lives in the taskbar: a single broker gets its own chip (shown always by default; Control Panel → Desktop → Broker status chip can limit it to "Only when a broker needs attention" or "Never") (green ok / red down / amber password-needed / blue lease-inactive; the chip doubles as its hide toggle), while two or more collapse into one aggregate badge — the worst state colors it, and clicking it opens the start (+) menu where every broker has a live status row carrying those same actions. Requirements worth knowing:

  • Keep the fleet on one webterm version. Nothing refuses an older remote (every broker answers cross-origin requests with Access-Control-Allow-Origin: *), but a remote older than the page lacks newer routes and fields, so newer features degrade or hide for it (see Hosts-and-Multi-Browser → Mixed versions). A host reached from server needs no CORS at all: the browser only talks to the serving broker.
  • Pick ONE scheme for the whole fleet — and prefer https. An https page fetching an http broker is blocked by the browser as mixed content, so a half-and-half fleet breaks: the cockpit and every host must all be https, or all be plain http — for hosts the browser dials directly. A host reached from server (through the cockpit's own broker) is fetched on the cockpit's origin, so an https cockpit can reach a plain-http broker that way. tailscale serve on every machine is the easy all-https answer (an https proxy of your own works too). Https also makes the page a secure context, which some browser features require — clipboard image paste into a terminal (Alt+V / right-click) only works on an https or localhost page. All-plain-http over a trusted tailnet still works; you just forfeit those.

Full auth/CORS details: Technical Reference → Multiple hosts.

Don't hand-edit this

The single most common setup mistake (especially when an AI agent is driving) is editing the wrong thing. Rules of thumb:

  • Joining brokers is normally a UI action, not a config edit. To attach machine B, use Control Panel → Browser → Hosts in the browser. The one exception is peers (#252): an operator can name other brokers that this broker will relay to (/peer/<id>/), for a browser or phone that can reach only this broker — see Hosts-and-Multi-Browser#reaching-a-broker-through-this-one. Even then the browser still adds the host itself and holds its password; peers stores no credential.
  • broker_config.json describes this broker: its bind (host/port), auth_token, its role (serve_ui / terminals), MCP gates, the launchable profiles, and — optionally — the peers it relays to. Apart from peers, nothing in it points at other machines.
  • profiles are an allow-list, deliberately. The browser can only launch pre-approved profiles — it can never supply a raw command. The agent.profiles here are the seed; the easiest way to add a WSL/zsh/PowerShell profile is Control Panel → (a host tab) → Terminals → Launch profiles (add/edit/detect, applied live with no restart; not on a broker with terminals: false, which refuses profile edits), which persists to a webterm_profiles.json sidecar that then owns the set. See Launch-Profiles for copyable recipes and the sidecar-vs-broker_config rule.
  • Tokens/passwords for remote hosts live in the browser (localStorage), set through the add-host form — not in any file on disk.

If you're an agent and you find yourself about to edit broker_config.json to "connect to the other machine," stop: that's a Control Panel → Browser → Hosts action instead.

Running the broker in the background

A broker is a long-lived server; you usually want it running unattended and surviving logout — not tied to a terminal you happened to launch it from.

Windows (Task Scheduler — recommended)

Launching the broker from a restricted or unusual parent process (a sandbox, a service with a stripped profile, or another tool's subprocess) can hand its child terminals the wrong environment — bad permissions on user-profile paths (PSReadLine history, Codex/agent state, application logs) and stray inherited environment variables. The fix is to start the broker from a clean, normal user context. Task Scheduler does exactly that.

Create a task that runs at logon as your own user:

# adjust the path to your checkout and python
$action  = New-ScheduledTaskAction -Execute "python" `
             -Argument "-m webterm.broker" `
             -WorkingDirectory "X:\path\to\browserland"
$trigger = New-ScheduledTaskTrigger -AtLogOn
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
             -DontStopIfGoingOnBatteries -StartWhenAvailable
Register-ScheduledTask -TaskName "Browserland broker" `
  -Action $action -Trigger $trigger -Settings $settings `
  -RunLevel Limited                # run as a normal (non-elevated) user

Notes:

  • Run as the interactive user, non-elevated (-RunLevel Limited). Running the broker elevated or as SYSTEM is what causes the profile-path permission problems above — its spawned terminals inherit that context.
  • Don't launch the broker from another tool's subprocess if that tool injects its own environment. A broker started inside, say, a coding-agent session can leak that agent's marker variables into every terminal it spawns; Task Scheduler avoids this by starting from a clean session. If you must restart it from such a context, scrub the offending variables from the environment first.
  • Set auth_token and a tailnet bind in broker_config.json before the task starts binding non-loopback (see above).
  • Verify it's up: open http://127.0.0.1:4445/, or Get-ScheduledTask "Browserland broker".

Alternative: any non-restricted startup method works — a .lnk in shell:startup, NSSM-as-a-service, etc. The requirement is just clean, non-elevated, normal-profile — Task Scheduler is the least-surprising way to get there.

Linux (systemd)

Installable units ship in launchers/systemd/. See Technical Reference → Linux deployment for the full walkthrough (webterm-broker.service / webterm-agent.service, User=, paths, token).

See also

Clone this wiki locally