What port does the Gateway use?
`gateway.port` controls the single multiplexed port for WebSocket + HTTP (Control UI, hooks, etc.). Precedence:
```text
--port > APUS_GATEWAY_PORT > gateway.port > default 18789
```
Why does apus gateway status report a running process but a failed connection?
"Running" is the **supervisor's** view (launchd/systemd/schtasks); the connectivity check is the CLI actually connecting to the gateway WebSocket. In `apus gateway status`, compare the connection target URL with `Listening:` (what is actually bound on the port), and inspect `Last gateway error:` for the failure cause when the process is alive but the port is not listening.
Why does apus gateway status show "Config (cli)" and "Config (service)" different?
You are editing one config file while the service runs another (often a `--profile` / `APUS_STATE_DIR` mismatch).
Fix, run from the same `--profile` / environment you want the service to use:
```bash
apus gateway install --force
```
What does "another gateway instance is already listening" mean?
Apus enforces a runtime lock by binding the WebSocket listener immediately on startup (default `ws://127.0.0.1:18789`). If the bind fails with `EADDRINUSE`, it throws `GatewayLockError` ("another gateway instance is already listening").
Fix: stop the other instance, free the port, or run with `apus gateway --port <port>`.
How do I run Apus in remote mode (client connects to a Gateway elsewhere)?
Set `gateway.mode: "remote"` and point to a remote WebSocket URL, optionally with shared-secret remote credentials:
```json5
{
gateway: {
mode: "remote",
remote: {
url: "ws://gateway.tailnet:18789",
token: "your-token",
password: "your-password",
},
},
}
```
- `apus gateway` only starts when `gateway.mode` is `local` (or you pass an override flag).
- The macOS app watches the config file and switches modes live when these values change.
- `gateway.remote.token` / `.password` are client-side remote credentials only; they do not enable local gateway auth by themselves.
The Control UI says "unauthorized" (or keeps reconnecting). What now?
Your gateway auth path and the UI's auth method do not match.
Facts (from code):
- The Control UI keeps the token in `sessionStorage`, scoped to the current browser tab and selected gateway URL, so same-tab refreshes keep working without long-lived localStorage token persistence.
- On `AUTH_TOKEN_MISMATCH`, trusted clients can attempt one bounded retry with a cached device token when the gateway returns retry hints (`canRetryWithDeviceToken=true`, `recommendedNextStep=retry_with_device_token`).
- That cached-token retry reuses the cached approved scopes stored with the device token; explicit `deviceToken` / explicit `scopes` callers keep their requested scope set instead of inheriting cached scopes.
- Outside that retry path, connect auth precedence is explicit shared token/password first, then explicit `deviceToken`, then stored device token, then bootstrap token.
- Built-in setup-code bootstrap returns a node device token with `scopes: []` plus a bounded operator handoff token for trusted mobile onboarding. The operator handoff can read setup-time native configuration but does not grant pairing mutation scopes or `operator.admin`.
Fix:
- Fastest: `apus dashboard` (prints + copies the dashboard URL, tries to open; shows an SSH hint if headless).
- No token yet: `apus doctor --generate-gateway-token`.
- Remote: tunnel first with `ssh -N -L 18789:127.0.0.1:18789 user@host`, then open `http://127.0.0.1:18789/`.
- Shared-secret mode: set `gateway.auth.token` / `APUS_GATEWAY_TOKEN` or `gateway.auth.password` / `APUS_GATEWAY_PASSWORD`, then paste the matching secret in Control UI settings.
- Tailscale Serve mode: confirm `gateway.auth.allowTailscale` is enabled and you are opening the Serve URL, not a raw loopback/tailnet URL that bypasses Tailscale identity headers.
- Trusted-proxy mode: confirm you are coming through the configured identity-aware proxy. Same-host loopback proxies also need `gateway.auth.trustedProxy.allowLoopback = true`.
- Mismatch persists after the one retry: rotate/re-approve the paired device token:
```bash
apus devices list
apus devices rotate --device <id> --role operator
```
- Rotate denied: paired-device sessions can rotate only their **own** device unless they also have `operator.admin`, and explicit `--scope` values cannot exceed the caller's current operator scopes.
- Still stuck: `apus status --all` plus [Troubleshooting](/gateway/troubleshooting). See [Dashboard](/web/dashboard) for auth details.
I set gateway.bind tailnet but it listens only on loopback
`tailnet` bind picks a Tailscale IP from your network interfaces (100.64.0.0/10). If the machine is not on Tailscale (or the interface is down), the Gateway falls back to loopback instead of exposing another network interface.
Fix: start Tailscale on that host and restart the Gateway, or switch explicitly to `gateway.bind: "loopback"` / `"lan"`.
`tailnet` is explicit; `auto` prefers loopback. Use `gateway.bind: "tailnet"` to limit non-loopback exposure to the Tailnet while retaining the required same-host `127.0.0.1` listener.
Can I run multiple Gateways on the same host?
Usually no - one Gateway can run multiple messaging channels and agents. Use multiple Gateways only for redundancy (for example a rescue bot) or hard isolation, and isolate each with its own `APUS_CONFIG_PATH`, `APUS_STATE_DIR`, `agents.defaults.workspace`, and unique `gateway.port`.
Recommended: `apus --profile <name> ...` per instance (auto-creates `~/.apus-<name>`), a unique `gateway.port` per profile config (or `--port` for manual runs), and a per-profile service with `apus --profile <name> gateway install`.
Profiles also suffix service names: launchd `ai.apus.<profile>`, systemd `apus-gateway-<profile>.service`, Windows `Apus Gateway (<profile>)`. The unqualified `apus-gateway` systemd unit only exists for the default profile; the legacy pre-rename systemd unit name `clawdbot-gateway` is migrated automatically.
Full guide: [Multiple gateways](/gateway/multiple-gateways).
What does "invalid handshake" / code 1008 mean?
The Gateway is a **WebSocket server** and expects the first message to be a `connect` frame. Anything else closes the connection with **code 1008** (policy violation).
Common causes: you opened the **HTTP** URL in a browser instead of a WS client, used the wrong port/path, or a proxy/tunnel stripped auth headers or sent a non-Gateway request.
Fix: use the WS URL (`ws://<host>:18789`, or `wss://...` over HTTPS), do not open the WS port in a normal browser tab, and include the token/password in the `connect` frame when auth is on. CLI/TUI example:
```bash
apus tui --url ws://<host>:18789 --token <token>
```
Protocol details: [Gateway protocol](/gateway/protocol).