docs: document self-hosting the web UI and community projects

Documents the daemon-served web UI from #1635: enabling it, reverse
proxy, TLS, tunnels, and the auth/exposure model. Adds a community
projects page for community-built self-hosting tooling.
This commit is contained in:
Mohamed Boudra
2026-06-26 11:35:08 +07:00
parent a49c658d1f
commit c5c2ace698
6 changed files with 247 additions and 1 deletions

View File

@@ -50,7 +50,7 @@ The heart of Paseo. A Node.js process that:
- Streams agent output in real time via a timeline model
- Provides agent-to-agent tools through a transport-neutral tool catalog, with MCP as one adapter
- Optionally connects outbound to a relay for remote access
- Optionally serves the browser web client from the same HTTP server
- Optionally serves the browser web client from the same HTTP server (self-hosting guide: [public-docs/web-ui.md](../public-docs/web-ui.md))
All paths are under `packages/server/src/`.

View File

@@ -238,6 +238,8 @@ Service proxy hostnames use the double-dash shape: `web--feature-auth--project.l
## Bundled daemon web UI
> The user-facing guide for this feature (enabling it, reverse proxy, TLS, tunnels, security) lives at [public-docs/web-ui.md](../public-docs/web-ui.md). This section is the contributor/build reference: how the artifact is produced, bundled, and excluded from desktop packaging.
The daemon can optionally serve the browser web client from the same HTTP server. This is disabled by default.
Enable it for a running daemon with:

21
public-docs/community.md Normal file
View File

@@ -0,0 +1,21 @@
---
title: Community projects
description: Community-built tools and integrations for Paseo, including self-hosted Docker builds and an alternative relay.
nav: Community
order: 7
category: Getting started
---
# Community projects
Projects built by the Paseo community. These **aren't official Paseo projects** and aren't covered by Paseo's support, but they're useful starting points, especially for self-hosting. Review the code before running anything that touches your machine or your agents.
## Self-hosting
- **[blockfeed/paseo-selfhosted](https://github.com/blockfeed/paseo-selfhosted)**, a Docker build that runs the Paseo web UI connected to a self-hosted local daemon. A good reference if you want a containerized setup. For the built-in way to serve the UI from the daemon, see [Self-hosting the web UI](/docs/web-ui).
- **[zenghongtu/paseo-relay](https://github.com/zenghongtu/paseo-relay)**, a lightweight self-hosted relay server for Paseo, written in Go. Run your own relay instead of the hosted one for fully self-hosted remote access. For how the relay fits into Paseo's connection model, see [Security](/docs/security).
## Add your project
Built something for Paseo, a relay, a deployment recipe, an integration, a client? Open a pull request or an issue on [GitHub](https://github.com/getpaseo/paseo) to get it listed here.

View File

@@ -27,6 +27,8 @@ paseo
Paseo prints a QR code in the terminal. Scan it from the mobile app, or enter the daemon address manually from another client.
The daemon can also serve the browser web app itself, so you can use the full UI without the hosted app. See [Self-hosting the web UI](/docs/web-ui).
Configuration and local state live under `PASEO_HOME` (defaults to `~/.paseo`).
## Where next
@@ -34,6 +36,7 @@ Configuration and local state live under `PASEO_HOME` (defaults to `~/.paseo`).
- [Workspaces](/docs/workspaces), the project, workspace, and session model Paseo is built around.
- [Providers](/docs/providers), what a provider is and how Paseo wraps existing CLIs.
- [CLI reference](/docs/cli), every command.
- [Self-hosting the web UI](/docs/web-ui), serve the browser app from your own daemon.
- [GitHub repo](https://github.com/getpaseo/paseo)
- [Report an issue](https://github.com/getpaseo/paseo/issues)

View File

@@ -93,6 +93,8 @@ By default, anyone who can reach the daemon's listening address can connect. On
When a password is configured, all HTTP requests must include an `Authorization: Bearer <password>` header and all WebSocket connections must authenticate via subprotocol. Unauthenticated requests receive a `401 Unauthorized` response. Only the `/api/health` liveness endpoint is exempt, so that process supervisors and load balancers can probe without credentials.
If you enable the [bundled web UI](/docs/web-ui), its static files are also served without the password so the login screen can render. This is by design, the API and WebSocket still require authentication before any agent data is returned or any command runs. Set a password before binding the daemon to a network so the data behind the page stays protected.
The password is stored as a bcrypt hash in `config.json`, the daemon never stores it in plaintext. See [Configuration](/docs/configuration#password-authentication) for setup instructions.
### What password auth does and does not do

218
public-docs/web-ui.md Normal file
View File

@@ -0,0 +1,218 @@
---
title: Self-hosting the web UI
description: Serve the Paseo web app from your own daemon and reach it over your own LAN, VPN, reverse proxy, or tunnel.
nav: Web UI
order: 6
category: Getting started
---
# Self-hosting the web UI
Paseo's daemon can serve the browser web app itself, from the same address it already uses for the API. You don't need the hosted app at [app.paseo.sh](https://app.paseo.sh): point a browser at your own daemon and you get the full UI, connected to your own agents, on infrastructure you control.
This is useful when you want to:
- Run the whole UI on your own machine or server.
- Put it behind your own reverse proxy, HTTPS, or tunnel.
- Keep the setup self-hosted end to end, with no dependency on the hosted web app.
The web app ships inside the daemon package, so the UI you serve always matches your daemon version. There's no separate build to keep in sync and no UI-vs-daemon version skew to manage.
## Enable it
The bundled web UI is off by default. Turn it on when you start the daemon:
```bash
paseo daemon start --web-ui
```
Or with an environment variable:
```bash
PASEO_WEB_UI_ENABLED=true paseo daemon start
```
Or persist it in `config.json` so it survives restarts:
```json
{
"features": {
"webUi": {
"enabled": true
}
}
}
```
Then open the daemon's address in a browser:
```
http://localhost:6767/
```
If your daemon doesn't recognize `--web-ui`, update it, the flag was added with the bundled web UI.
## How the connection works
The page is served from the same origin as the daemon's API and WebSocket. When you open it, the app automatically connects back to that same origin, so you usually skip the "Add Host" step entirely, open `http://localhost:6767/` and you're looking at your agents.
The same HTTP server keeps serving the API (`/api/*`), MCP (`/mcp/*`), service-proxy routes, and the WebSocket upgrade. Only the static files are new. To point the served UI at a _different_ daemon, add that daemon as a host from the UI as usual.
## Topologies
Three common ways to run it, in order of exposure:
- **Same machine.** Daemon and browser on one box. Open `http://localhost:6767/`. Nothing else to configure.
- **Private network (LAN or VPN).** Reach the daemon from other devices on a network you trust, a home LAN or a [Tailscale](https://tailscale.com) tailnet. Bind the daemon to that interface and connect to it by address.
- **Public reverse proxy or tunnel.** Expose the UI on a domain over HTTPS, terminating TLS at a reverse proxy or a tunnel. This is the full self-hosted setup.
The rest of this page builds from local to public. **Verify a direct connection works before you add a proxy in front of it**, it isolates daemon problems from proxy problems.
## Exposing beyond localhost
By default the daemon listens on `127.0.0.1:6767`, reachable only from the same machine. To reach it from other devices, bind it to a network interface:
```bash
paseo daemon start --web-ui --listen 0.0.0.0:6767
```
> **Anyone who can reach the listening address can use your agents.** Before you bind beyond localhost, set a password and review your host allowlist. The relay (the default mobile pairing path) avoids this entirely by keeping the daemon bound to localhost, see [Security](/docs/security).
Two things to configure when you expose the daemon directly:
1. **Set a password** so only authorized clients can connect:
```bash
PASEO_PASSWORD=my-secret paseo daemon start --web-ui --listen 0.0.0.0:6767
```
See [password authentication](/docs/configuration#password-authentication) for the persistent setup. Password auth controls access; it does not encrypt traffic, put TLS in front of it (below) on any untrusted network.
2. **Allow your hostname** so the daemon's DNS-rebinding protection accepts requests for your domain:
```bash
paseo daemon start --web-ui --listen 0.0.0.0:6767 --hostnames ".example.com"
```
See [DNS rebinding protection](/docs/security#dns-rebinding-protection) for how the host allowlist works.
> **The web app loads before authentication, by design.** The static UI files are served without the daemon password so the login screen can render; the API and WebSocket still require the password before any agent data is returned or any command runs. Don't treat "the page loaded" as "the daemon is open", but do set a password before binding to a network so the data behind the page stays protected.
## Reverse proxy
To serve the UI on a domain over HTTPS, terminate TLS at a reverse proxy and forward everything to the daemon. Keep the daemon on localhost and let the proxy be the only thing exposed.
A working proxy must:
- **Forward the WebSocket upgrade.** The app streams agent output over a WebSocket at `/ws`; without upgrade support the UI loads but never connects.
- **Not buffer responses.** Terminal output and other live streams are long-lived; buffering makes the UI look frozen.
- **Use long read timeouts.** Those streams stay open for the life of a session.
- **Allow large request bodies.** Prompts and file uploads can be big.
- **Preserve the `Host` header and pass `X-Forwarded-Proto`.** The daemon uses these to tell the app which origin and scheme (`wss://` vs `ws://`) to connect back on. Drop them and auto-connect points at the wrong place.
### Nginx
```nginx
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
server_name paseo.example.com;
ssl_certificate /etc/letsencrypt/live/paseo.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/paseo.example.com/privkey.pem;
client_max_body_size 100m;
location / {
proxy_pass http://127.0.0.1:6767;
proxy_http_version 1.1;
# WebSocket upgrade
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Preserve origin + scheme so the UI connects back over wss://
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Long-lived, unbuffered streams
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
```
### Caddy
Caddy handles TLS, the WebSocket upgrade, header forwarding, and streaming for you:
```caddy
paseo.example.com {
reverse_proxy 127.0.0.1:6767
}
```
That's the whole config. Caddy provisions a certificate automatically and preserves `Host` and `X-Forwarded-Proto` by default.
## HTTPS and TLS
Terminate TLS at the proxy (or tunnel) and forward to the daemon over plain HTTP on localhost, that's what the configs above do. When the page is served over HTTPS and the proxy passes `X-Forwarded-Proto: https`, the app automatically connects back over `wss://`. You don't configure the scheme anywhere; it follows the edge.
The daemon honors the forwarded scheme only from a proxy on the **same host** (a loopback connection), which is what all the setups above do, the proxy or tunnel forwards to `127.0.0.1:6767`. Keep the proxy co-located with the daemon. If you must run the proxy on a different machine, the daemon won't trust its forwarded scheme; in that case add the daemon as a host manually in the UI with TLS enabled.
If you serve the UI over HTTPS but the app tries to connect over `ws://` (and the browser blocks it as mixed content), your proxy isn't on the daemon's host or isn't forwarding `X-Forwarded-Proto`. Fix whichever applies.
For the remote/relay path (driving a daemon through the Paseo relay rather than a reverse proxy), the relay has its own public-vs-internal TLS settings, see [Security](/docs/security).
## Tunnels
If you don't want to manage a reverse proxy or open ports, a tunnel gives you an HTTPS URL that forwards to your local daemon.
- **Tailscale Serve** keeps it inside your tailnet, no public exposure, TLS handled for you:
```bash
tailscale serve https / http://127.0.0.1:6767
```
Reach it at `https://<your-machine>.<tailnet>.ts.net/`. Only devices on your tailnet can connect.
- **Cloudflare Tunnel** exposes it on a public hostname with TLS and WebSocket support:
```bash
cloudflared tunnel --url http://localhost:6767
```
Cloudflare terminates TLS and sets `X-Forwarded-Proto: https`, so auto-connect works. Because the URL is public, **set a daemon password.**
## Security
Self-hosting the web UI puts you in charge of who can reach the daemon. The essentials:
- **Set a password before binding beyond localhost.** The static page loads without it, but agent data and commands stay behind it. See [Security](/docs/security#password-authentication).
- **Put TLS in front of any untrusted network.** Password auth protects access, not confidentiality.
- **Keep the daemon on localhost when you can** and let a reverse proxy or tunnel be the only exposed surface.
- **Review your host allowlist** when serving on a custom domain.
For the full threat model, relay encryption, and DNS-rebinding details, see [Security](/docs/security) and [SECURITY.md](https://github.com/getpaseo/paseo/blob/main/SECURITY.md).
## Troubleshooting
- **Blank page or 404 at `/`.** The web UI isn't enabled. Start the daemon with `--web-ui` and confirm with `paseo daemon status` that it's the daemon you're hitting.
- **Page loads but never connects.** The proxy isn't forwarding the WebSocket upgrade, or it's stripping the `Host` header. Check the upgrade headers in your proxy config.
- **Connects, then output freezes.** Response buffering is on, or read timeouts are too short. Disable buffering and raise the timeouts.
- **"Mixed content" / connection blocked over HTTPS.** The app fell back to `ws://`. Either the proxy isn't sending `X-Forwarded-Proto: https`, or it isn't running on the daemon's host (the daemon only trusts the forwarded scheme from a loopback proxy). Co-locate the proxy with the daemon and forward the header.
- **`403 Invalid Host header`.** Your domain isn't in the allowlist. Add it with `--hostnames` or `daemon.hostnames`, see [DNS rebinding protection](/docs/security#dns-rebinding-protection).
- **Large prompts or uploads fail.** Raise the proxy's max body size (`client_max_body_size` in Nginx).
## See also
- [Security](/docs/security), connection methods, relay encryption, password auth, host allowlist.
- [Configuration](/docs/configuration), `config.json`, environment variables, and CLI overrides.
- [CLI](/docs/cli), the `paseo daemon` commands.
- [Community projects](/docs/community), community-built self-hosting tooling.