diff --git a/docs/how-to/heph-pwa.md b/docs/how-to/heph-pwa.md
index dbc19ef..2a158e9 100644
--- a/docs/how-to/heph-pwa.md
+++ b/docs/how-to/heph-pwa.md
@@ -112,6 +112,7 @@ Search (🔍 or `/`) runs full-text search across tasks and docs.
## Related
+- [[host-heph-pwa]] — serve this app from the hub (indri) with OIDC, in the hub/spoke deployment
- [[set-up-sync-hub]] — stand up the server-mode hub the app talks to
- [[run-the-daemon]] — run `hephd` as a managed service
- [[v1-prototype-tech-spec]] — data model, RPC API, quick-add spec
diff --git a/docs/how-to/host-heph-pwa.md b/docs/how-to/host-heph-pwa.md
new file mode 100644
index 0000000..0be1d59
--- /dev/null
+++ b/docs/how-to/host-heph-pwa.md
@@ -0,0 +1,127 @@
+---
+title: Host heph-pwa from the hub
+modified: 2026-06-04
+tags:
+ - how-to
+---
+
+# Host heph-pwa from the hub
+
+How to serve the [[heph-pwa]] mobile app from the canonical **hub** (`indri`) in
+the hub-and-spoke deployment, with OIDC auth — the production counterpart of the
+unauthenticated single-machine demo. Assumes the `heph-pwa` work is **merged and
+released**, so the installed `hephd` already has `--web-root` and CORS.
+
+> Read [[set-up-sync-hub]] first — this builds directly on the hub it stands up
+> (server mode, Authentik OIDC, Tailscale transport).
+
+## What the app needs from the hub
+
+The PWA is a thin, online-only client: it loads its static shell over HTTP and
+makes JSON-RPC calls to the hub's `/rpc`. So the hub must (1) serve the shell
+files and (2) accept the app's authenticated RPC calls. Both are already in
+`hephd --mode server`:
+
+- `--web-root
` serves the shell for any non-API path (with an `index.html`
+ SPA fallback). The shell is unauthenticated — it is only HTML/JS; all data
+ still flows through the OIDC-gated `/rpc`.
+- Every response carries permissive CORS headers and answers the `OPTIONS`
+ preflight, so the shell may instead be hosted anywhere and still call the hub
+ cross-origin.
+
+## 1. Put the shell on the hub
+
+The release does not yet bundle the app, so fetch the `heph-pwa/` directory at
+the **same version tag** the hub runs (keeping shell and hub in lockstep matters
+— see *Upgrades* below), and copy it to a stable path:
+
+```bash
+# on indri, matching the running hephd version (e.g. v1.4.0)
+git clone --depth 1 --branch v1.4.0 \
+ https://forge.ops.eblu.me/eblume/hephaestus.git /tmp/heph-src
+sudo mkdir -p /var/lib/heph/web
+sudo cp -r /tmp/heph-src/heph-pwa/. /var/lib/heph/web/
+```
+
+> **Future improvement:** have the release workflow package a `heph-pwa-.tar.gz`
+> asset (as it already does for docs), so this step becomes "download + extract"
+> and the lockstep is automatic. Until then, pin the clone to the hub's tag.
+
+## 2. Add `--web-root` to the hub service
+
+Extend the hub invocation from [[set-up-sync-hub]] with `--web-root` (everything
+else — issuer, audience, db — unchanged):
+
+```bash
+hephd --mode server \
+ --http-addr 0.0.0.0:8787 \
+ --db /var/lib/heph/heph.db \
+ --web-root /var/lib/heph/web \
+ --oidc-issuer https://authentik.ops.eblu.me/application/o/heph/ \
+ --oidc-audience
+```
+
+In the systemd unit (or launchd plist), add the two `--web-root` arguments and
+`systemctl restart hephd`. Self-update is compatible now that the release ships
+the flag — just refresh the web-root on each upgrade (next section).
+
+## 3. Terminate TLS (recommended)
+
+Serve the app over **HTTPS** so it is a *secure context*: only then do the
+service worker (offline launch), proper PWA install, and the Web Speech mic
+work. (On iOS, "Add to Home Screen" and keyboard dictation work over plain HTTP
+too, so HTTPS is a polish step, not a blocker.) Two good options:
+
+- **Tailscale serve** — tailnet-only, automatic MagicDNS cert, no public
+ exposure:
+
+ ```bash
+ tailscale serve --bg --https=443 http://127.0.0.1:8787
+ # app is then at https://indri..ts.net/
+ ```
+
+ Bind `hephd` to `127.0.0.1:8787` in this case and let Tailscale be the only
+ thing exposing it.
+
+- **Reverse proxy** (Caddy / nginx) terminating a real cert, if the hub should
+ be reachable beyond the tailnet. Proxy all paths (`/`, `/rpc`, `/sync/*`) to
+ `hephd`.
+
+Either way the app is same-origin with the hub, so no CORS is involved and the
+app defaults its hub URL to its own origin.
+
+## 4. Connect a phone
+
+1. Ensure the phone is on the tailnet (or can reach the proxy).
+2. Open the hub URL (`https://indri..ts.net/`) and **Add to Home Screen**.
+3. The app defaults its **Hub URL** to the origin it loaded from — no typing.
+4. **Token:** the hub requires an OIDC bearer token, and the PWA does **not yet
+ implement the in-app device-code login** — paste a token into Settings →
+ Token for now. Obtain one via the device-code flow against the Authentik
+ client (the same flow the CLI uses; e.g. reuse the access token a logged-in
+ spoke cached, or run a one-off device-code grant). Tap **Test** to confirm.
+
+> **Known gap / next step:** wire the RFC 8628 device-code flow into the PWA's
+> Settings so login is in-app (open the verification URL, poll for the token,
+> store it, and refresh it) — removing the manual paste. Tracked as follow-up
+> work for `heph-pwa`.
+
+## Upgrades
+
+On each hub upgrade, refresh the shell so it matches the running `hephd`:
+
+```bash
+git -C /tmp/heph-src fetch --depth 1 origin v1.5.0 && git -C /tmp/heph-src checkout v1.5.0
+sudo cp -r /tmp/heph-src/heph-pwa/. /var/lib/heph/web/
+```
+
+The service worker is versioned (`CACHE = "heph-pwa-vN"`), so an updated shell
+evicts the old cache on next load. Hard-refresh once if a phone seems stuck on a
+stale version.
+
+## Related
+
+- [[heph-pwa]] — the app itself (features, quick-add, voice, triage)
+- [[set-up-sync-hub]] — stand up the hub + Authentik OIDC this doc extends
+- [[run-the-daemon]] — run `hephd` as a managed service
+- [[v1-prototype-tech-spec]] — RPC API and auth model
diff --git a/docs/how-to/how-to.md b/docs/how-to/how-to.md
index 62c0173..c20c904 100644
--- a/docs/how-to/how-to.md
+++ b/docs/how-to/how-to.md
@@ -22,3 +22,4 @@ Task-oriented guides for common operations.
- [[import-todoist]] — Seed a heph store from your Todoist projects + tasks (`mise run import-todoist`)
- [[self-update]] — Opt-in `hephd` self-update: poll the forge for new releases and auto-update
- [[heph-pwa]] — The mobile app: an installable PWA mirror of heph-tui (browse, triage, fast quick-add, voice)
+- [[host-heph-pwa]] — Serve the mobile app from the hub (indri) with OIDC, in the hub/spoke deployment