Documentation Index
Fetch the complete documentation index at: https://docs.yourhq.ai/llms.txt
Use this file to discover all available pages before exploring further.
Networking & Deployment Topology
This is the “how do I reach HQ?” reference. It covers the mental model for HQ’s network surface, the three installer-supported modes, multi-machine topologies, and options for exposing HQ to the public internet with a custom domain. If you’re just starting out: run the installer, pick Local-only or Tailscale, and come back here when you want to split services across machines or put HQ on a custom domain.1. The mental model
Tailscale lives on the HOST. HQ containers don’t know it exists. This is the single most important idea in this doc. HQ is a plain Docker Compose stack that publishes TCP ports. What happens to those ports — whether they’re reachable only from loopback, from your tailnet, or from the public internet — is decided by two things:- Host port bindings in
docker-compose.yml(UI_HOST_PORT,NOVNC_HOST_PORT,FILES_API_HOST_PORT). Each one looks like127.0.0.1:3000or0.0.0.0:3000.127.0.0.1means loopback only;0.0.0.0means every network interface the host has. - The host’s networking setup. If you installed Tailscale on the host,
0.0.0.0now includes the Tailscale interface. If you opened firewall ports, it includes the public internet.
2. The three networking modes
The installer (installer/install.sh) asks which mode to use and writes the
right .env values. You can switch later by editing .env and running
docker compose up -d again.
| Mode | HOST_REACHABLE_URL | Host port bindings | Reachable from |
|---|---|---|---|
local (default) | http://localhost | 127.0.0.1:3000 / :6901 / :18790 | This machine only |
tailscale | http://<host-ts-ip> | 0.0.0.0:3000 / :6901 / :18790 | Any tailnet device |
public | https://<your-domain> | 0.0.0.0:3000 / :6901 / :18790 | Anywhere — you handle TLS |
Local-only (default)
The simplest setup. Everything binds to127.0.0.1 on the host, so only this
machine can reach HQ.
Install:
http://localhost:3000
Tradeoffs:
- Zero attack surface — nothing on the network can touch HQ
- No phone / tablet / remote laptop access
- No way to split gateway onto another host
- Best for: solo laptop use, first-time trial, airgapped setups
Tailscale (recommended for multi-device / multi-host)
The installer installs the Tailscale client on the host (not in a container), logs into your tailnet with an auth key, and flips the host port bindings to0.0.0.0. HQ is now reachable from any device on your tailnet
(phone, laptop, other VPSes) but not from the public internet.
Install:
http://<host-ts-ip>:3000— always works, even if MagicDNS is offhttp://yourhq-<hostname>:3000— works if MagicDNS is on (see §5)
- Access from anywhere you’re logged into Tailscale
- No public exposure — the attack surface is just your tailnet
- Gives you exit-node support for free (see §9)
- Devices need the Tailscale app/login
- Best for: anyone beyond a single machine, and the default recommendation
Public HTTPS (advanced)
You run a reverse proxy (Caddy, Traefik, nginx, Cloudflare Tunnel — your choice) on the host, in front of HQ’s ports. The installer just flips the host port bindings to0.0.0.0, sets HOST_REACHABLE_URL=https://your.domain,
and gets out of the way.
HQ does not bundle a reverse proxy anymore. An earlier version shipped
Caddy inside the gateway container; we removed it so the stack stays small
and TLS/domain concerns live where they belong (on the host).
Install:
- Custom domain and full public reach
- You own the TLS certs, DNS, and reverse proxy config
- You also own the risk (see §8)
- Best for: sharing HQ with users who can’t run Tailscale
3. Port inventory
| Service | Container port | Default host port | Protocol | What speaks it |
|---|---|---|---|---|
| UI (Next.js) | 3000 | UI_HOST_PORT (default 127.0.0.1:3000) | HTTP | Your browser |
| noVNC (websockify) | 6901 | NOVNC_HOST_PORT (default 127.0.0.1:6901) | HTTP + WebSocket | Browser — opens vnc.html to remote-desktop into the gateway |
| Files-API (Python) | 18790 | FILES_API_HOST_PORT (default 127.0.0.1:18790) | HTTP | UI’s file-browser backend (agent worktrees) |
| Gateway dispatcher | — (no listener) | — | — | Polls Supabase only |
| Gateway runner | — (no listener) | — | — | Polls Supabase only |
.env. Nothing in HQ runs on Supabase’s behalf.
The dispatcher and runner containers don’t listen on any port. They poll
Supabase over HTTPS for inbox items and commands. No inbound firewall holes
required for them.
4. Host bindings via compose
Three env vars in.env control the host side of the port mappings:
docker compose up -d reapplies the mapping.
NOVNC_BIND inside the container
Separate from host port bindings, NOVNC_BIND controls what websockify does
inside the container:
local(default) — websockify binds0.0.0.0:6901inside the container. Docker’s port mapping then enforces loopback-vs-public at the host.off— websockify doesn’t start. Use on headless hosts where you never want the noVNC console.
5. MagicDNS
Tailscale ships a built-in DNS server (MagicDNS). When enabled in your tailnet’s admin console, every device gets a hostname of its own — and the host the installer ran on getsyourhq-<hostname> (set via the --hostname
flag passed to tailscale up).
With MagicDNS on, from any tailnet device you can visit:
http://100.x.y.z) into
HOST_REACHABLE_URL instead of the hostname: MagicDNS can be disabled at the
tailnet level, devices may have stale DNS caches, and the IP always resolves.
The hostname is nicer for humans; the IP is a safer default for programmatic
use (the UI reads HOST_REACHABLE_URL from the gateways table to build
links to the files-API and noVNC console).
If MagicDNS is on and you prefer hostnames, edit .env:
docker compose up -d gateway. The gateway will re-register
with the hostname-based URL on its next boot.
6. Multi-machine topologies
The UI, gateway, dispatcher, and runner can all run on different hosts. Coordination is only through Supabase — nothing opens a socket to anything else in HQ. This is what makes splitting across machines painless. Shared rules:- Every host that runs anything needs Tailscale (or some other way for the UI to reach the gateway’s files-API and noVNC).
- Every gateway host needs a unique
GATEWAY_IDin its.env. - Every host uses the same
SUPABASE_URLandSUPABASE_SERVICE_ROLE_KEY. - The
gatewaystable in Supabase stores each gateway’sHOST_REACHABLE_URL(written by the gateway’s entrypoint on boot); the UI reads it at runtime to build file-browser and noVNC links.
Topology A: single machine (default)
docker compose up -d. The UI reaches the gateway over Docker’s internal
network via the gateway:18790 DNS name — no host port needed.
Topology B: UI on laptop, gateway on a VPS
Topology C: one UI, multiple gateways
GATEWAY_ID and GATEWAY_LABEL values. Each gateway self-registers in the
gateways table; the UI shows them all.
Use cases:
homegateway for local dev agents with a residential IPvps-eugateway for agents that need to run 24/7mac-minigateway for agents that need macOS-specific tooling
7. Custom domains and public access
HQ doesn’t bundle anything here — you pick the tool. Listed best-first for typical use cases. The options below all work today. Gateway URL overrides can be managed from Settings → Gateways; host-level reverse proxy and tunnel setup is still done outside HQ.Option A: Tailscale Serve — https://hq.<tailnet>.ts.net
Best for: remote access without public exposure. Tailnet-only users.
One command on the host gives you HTTPS on a stable .ts.net hostname.
Tailscale provisions and renews the cert. No ports opened to the public
internet.
- No custom domain — you get
<machine-name>.<tailnet>.ts.net - Requires the Tailscale app/login on every device that accesses it
- Free, no quota limits
- One command, no config files
Option B: Tailscale Funnel — public .ts.net hostname
Best for: sharing HQ with a few non-Tailscale users over a public URL,
without running your own domain or proxy.
Same as Serve but makes the hostname public (reachable from the open internet
with no Tailscale required on the client).
- Still no custom domain (you get
<name>.<tailnet>.ts.net) - Tight quotas on the free plan (check current limits)
- Public internet reach with TLS handled for you
Option C: Cloudflare Tunnel — custom domain, zero open ports
Best for: custom domain + max security. The host opens no inbound ports;cloudflared makes an outbound connection to Cloudflare and Cloudflare
routes HTTPS to your host.
High-level setup:
- Zero inbound ports on the host (huge security win)
- Requires a Cloudflare account + DNS on Cloudflare
- Free tier is generous
- Adds Cloudflare as a dependency in your request path
Option D: Caddy / Traefik / nginx — classic reverse proxy
Best for: custom domain + full control of the TLS stack. Point DNS at the host, open ports 80 and 443, let the proxy handle ACME. ExampleCaddyfile for hq.example.com:
- Ports 80/443 open to the public internet
- UI directly reachable by anyone on the internet, behind Supabase auth only
- Full control, classic ops workflow
- See §8 before going live
8. Security considerations for public access
When you put HQ on the public internet (option C or D above), the threat model changes. A few things to know:- Supabase auth is the only gate. The UI’s access control is “are you signed into the Supabase project?” Turn on MFA for your Supabase account.
- RLS is “authenticated full access.” Every policy in HQ’s schema grants authenticated users full CRUD on every table. That’s fine for a single-user workspace, dangerous for multi-user. Make your Supabase signup invite-only (Supabase dashboard → Authentication → Sign up: disabled, then invite by email). Otherwise anyone who can create a Supabase account in your project gets the whole workspace.
- Add a second auth layer if you can. Cloudflare Access (free for up to 50 users) puts an email-gated login in front of everything. Configure it on your Cloudflare Tunnel or in front of Caddy. That way an unauthenticated attacker can’t even reach the Next.js app.
- Don’t expose noVNC or files-API publicly. These are internal surfaces
with weaker auth. Keep them on Tailscale or loopback. If you use a reverse
proxy, only proxy port 3000 (the UI); leave
NOVNC_HOST_PORTandFILES_API_HOST_PORTon127.0.0.1or100.x.y.z(tailnet). - Gateway auth token. The files-API on port 18790 checks
GATEWAY_AUTH_TOKEN. Generate a long one (openssl rand -hex 32) and don’t commit it. If it leaks, anyone who can reach port 18790 can read and write your agent worktrees.
9. Exit nodes (Tailscale)
Some sites block or throttle datacenter IPs (LinkedIn, Google anti-abuse, some geo-restricted content). Tailscale exit nodes let you route outbound traffic from a host through another tailnet device — typically one in your home with a residential IP. The installer prompts for this when you pick Tailscale mode:HOST_REACHABLE_URL
on the gateway host). Only outbound traffic is affected.
10. Troubleshooting connectivity
”I can’t reach the UI from my phone”
Check the network path:- Is the phone on the same tailnet? Open the Tailscale app, verify it’s connected and the HQ host appears.
pingthe host’s tailnet IP from the phone (Tailscale app → “Ping”).- Check
UI_HOST_PORTin.envon the host — must be0.0.0.0:3000, not127.0.0.1:3000. Restart withdocker compose up -d uiafter editing. - Check host firewall — some VPS providers (Hetzner Cloud, AWS) block inbound 3000 by default, but Tailscale traffic goes through the WireGuard tunnel on UDP 41641 regardless. If Tailscale can reach the host at all, port 3000 should work. If only some devices can reach it, check the tailnet ACL.
”noVNC shows ‘Cannot assign requested address’ in logs”
NOVNC_BIND mismatch. Inside the container it should be local (binds
0.0.0.0:6901 inside, relying on the Docker port map to gate exposure).
Set NOVNC_BIND=local in .env and restart docker compose up -d gateway.
”UI hangs on login / redirects forever”
Almost always a Supabase URL / key mismatch. Check: UI Supabase config lives in the project registry, not.env. Open the UI,
go to Settings → Projects, and verify the active project’s URL and keys
are correct. To rotate or fix, use the Edit / Rotate buttons there —
no rebuild needed.
”Gateway doesn’t appear in the UI”
Check registration:registered (reachable at http://...) near the top of the
log. If you see registration failed (Supabase unreachable or gateways table missing), either:
- Supabase is unreachable from the gateway host (check
curl $SUPABASE_URL/rest/v1/from the host) - The
gatewaystable doesn’t exist — run every migration indb/migrations/in filename order from the Supabase SQL editor.
gateways table every ~30s; give it a refresh.
”Files browser shows ‘gateway unreachable’”
The UI reaches the gateway’s files-API viaGATEWAY_URL in .env.
- Same-host install: should be
http://gateway:18790(Docker DNS). - Remote gateway: should be the gateway host’s tailnet IP, e.g.
http://100.x.y.z:18790. Notlocalhost, not the Docker internal DNS.
GATEWAY_AUTH_TOKEN matches between the UI and gateway .env
files (both sides need the same value).
”My agent can’t reach a site from the gateway’s Chrome”
Two independent things:- DNS from the gateway container:
docker compose exec gateway getent hosts example.com. If that fails, the host’s DNS is wrong. - Outbound IP: if the site blocks datacenter IPs, use an exit node (§9).