Skip to content

PWA mode: installable client from two origins — localhost (zero TLS) + official hosted URL #511

Description

@itlackey

Re-scoped 2026-07-06 (ratified — see docs/technical/ui-client-split-assessment.md)

The PWA is the new @openpalm/client static app (adapter-static + @vite-pwa/sveltekit), not the adapter-node host build. Host features are absent from the artifact, not capability-hidden. Revised design: docs/technical/ui-runtime-modes-plan.md §6.10, Phase 6.

Two primary install origins (simplicity guardrail: TLS is never a manual user task on default paths)

  1. Localhost (desktop, zero setup). The harness/CLI serves the built client on a stable loopback port (PWA identity = origin incl. port). http://127.0.0.1 is a secure context → installable with no certificates. A loopback origin is exempt from mixed-content blocking and sits in the most-trusted Private Network Access tier, so it can talk to plain-HTTP LAN guardians. Entry points: Electron menu / host-app button ("Install OpenPalm app") and openpalm app.
  2. Official hosted URL (e.g. https://app.openpalm.dev). CI publishes the same static build to a canonical TLS-terminated origin — one install for any phone/tablet, connecting to any of the user's instances.

Platform constraints this design is built around (why the original "same build" scope changed)

  • Secure context: a PWA is only installable over HTTPS or from localhost/127.0.0.1. A UI served over plain HTTP on a LAN IP is never installable — the original plan had no answer for mobile install.
  • Mixed content: an HTTPS origin cannot fetch() plain-HTTP guardians → the hosted path requires guardian TLS (tracked in the Phase 6.5 workstream issue; Tailscale ts.net certs recommended default). Any phone → guardian connection effectively requires TLS.
  • Per-origin identity: one installed PWA = one origin; multi-instance switching must be in-app (the /connections switcher + IndexedDB store), which also means one hosted origin can serve all of a user's instances.

Acceptance criteria

  • @vite-pwa/sveltekit in packages/client: manifest, icons (192/512 + maskable), Workbox app-shell precache
  • Desktop: install prompt from http://127.0.0.1:<stable port> with zero TLS setup
  • Phone: install from the official hosted URL
  • Add/switch connections via paste-or-scan pairing (host app mints QR / one-time code — plan §6.6)
  • Hosted client refuses plain-HTTP non-loopback targets with an actionable message (deep-link to the TLS guide)
  • /api/runtime contract-version handshake: hosted client degrades gracefully against older instances
  • Guardian CORS allowlist includes the official origin (coordinates with Phase 6.5 issue)
  • Offline: app shell + saved connections (IndexedDB) render offline; no blank page; service worker never caches credentialed responses
  • When installed, clientDisplayMode resolves to standalone-pwa via matchMedia

Dependencies

Original scope (2026-06-20) — superseded by the re-scope above

Summary

Make the OpenPalm UI installable as a PWA on mobile and desktop. The PWA is the same SvelteKit build (adapter-node + service worker) — no new UI, no duplicate pages. Capabilities hide host controls when there is no local stack.

Background

The PWA install path enables a phone or tablet to install OpenPalm as an app that connects to a remote assistant (either an assistant-container instance or a full stack behind Guardian). This is the completion of #486 (remote-only install) on mobile.

Design

Full design in docs/technical/ui-runtime-modes-plan.md §6.10 and Phase 6.

What makes it a PWA

Two additions to the existing build:

  1. packages/ui/static/manifest.webmanifest — name, icons, display: standalone, start_url: /
  2. packages/ui/src/service-worker.ts — SvelteKit auto-registers; precaches app shell + static assets

No new pages. No separate build. No duplicate UI code.

How features are hidden

When hostMode === 'pwa-static', resolveCapabilities() returns only:

  • connections:manage, connections:switch, chat, pwa:install

All components check hasCapability(cap). Host-only nav items are absent because hasCapability('host:stack:read') is false — not because of branching UI code or separate routes.

Effective capability matrix for PWA

Active connection Available
None /connections/new (landing), pwa:install
Remote assistant chat, connections:switch, connections:manage
Remote assistant-container (future, via grantedCapabilities) + assistant-settings:read/write

Offline support

IndexedDB connection store: connection records are readable offline (cached), writes queued and replayed when reconnected.

Auth

Remote connections use Guardian Basic auth, coordinated with #435 Bearer static token seam.

Acceptance criteria

  • static/manifest.webmanifest present; browser shows install prompt on supported platforms
  • Service worker precaches app shell; offline shows shell (not blank page)
  • When installed as PWA, clientDisplayMode resolves to standalone-pwa via matchMedia
  • hasCapability('host:stack:read') returns false in PWA mode — host tabs not visible and not reachable
  • /api/host/* returns 403 even if a PWA client sends a direct request (server-enforced, not just hidden)
  • PWA can add a remote assistant connection (/connections/new) and open chat
  • PWA can switch between saved connections
  • Connection records readable offline (IndexedDB); no blank/crash on loss of connectivity
  • Icons provided at minimum 192×192 and 512×512 for install prompt

Dependencies

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions