hephaestus/docs/how-to/host-heph-pwa.md
Erich Blume 11aa25c9f4
All checks were successful
Build / validate (pull_request) Successful in 6m11s
feat(heph-tui,hephd): surface sync health (last-sync age, conflicts, auth failure)
A spoke could be silently failing to sync (expired token → 401, or hub
unreachable) with the only signal buried in the daemon log. Now:

- hephd tracks SyncHealth (last attempt/success time, last error, auth-failure
  flag) from the background sync loop and sync.now, classifying a 401 as an auth
  failure. sync.status returns it plus the pending merge-conflict count.
- heph-tui shows a live status-line indicator (spoke only): '⟳ <age>' since the
  last good sync, red '⚠ auth' when re-login is needed, '⚠ offline' when the hub
  is unreachable, and '⚠ N conflicts' when conflicts are pending. The event loop
  polls on a 2s tick so the age advances and failures appear while idle.
- docs: recommended Authentik access/refresh token validity to stop frequent
  re-logins (with the iOS PWA localStorage-eviction caveat).

Closes the 'Add hub connection status to heph-tui' and 'Spoke sync health:
surface unhealthy state instead of silent 401 spam' backlog items.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:19:11 -07:00

5.5 KiB

title modified tags
Host heph-pwa from the hub 2026-06-04
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 <dir> 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:

# 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-<version>.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):

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 <heph-client-id>

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).

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:

    tailscale serve --bg --https=443 http://127.0.0.1:8787
    # app is then at https://indri.<tailnet>.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.<tailnet>.ts.net/) and Add to Home Screen.

  3. The app defaults its Hub URL to the origin it loaded from — no typing.

  4. Sign in: open Settings → Login with Authentik. The app reads the hub's GET /config for the issuer + client id (zero-config) and runs an Authorization-Code + PKCE redirect to Authentik; after you approve it lands back on the app, signed in, and silently refreshes the token from then on. (A manual Bearer token field remains as a fallback for hubs without OIDC, or for pasting a one-off token.)

    Re-prompted for login too often? The fix is the Authentik provider's refresh token validity, not the app — see the token-lifetime note in set-up-sync-hub. (On iOS, Safari may also purge an un-installed PWA's storage after ~7 idle days; Add to Home Screen mitigates it.)

Prerequisite — register the PWA redirect URI. Browser PKCE needs the app's origin registered on the Authentik heph provider's Redirect URIs (Authentik also keys token-endpoint CORS off those origins). Add the PWA origin(s) with a trailing slash, e.g. https://heph.ops.eblu.me/ (and http://localhost:8787/ for local dev). In blumeops this is the redirect_uris list on the heph provider blueprint.

Upgrades

On each hub upgrade, refresh the shell so it matches the running hephd:

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.