Split: the frontend in its own container behind Caddy #203

Closed
opened 2026-10-11 20:01:38 -07:00 by rays · 2 comments
Owner

Part of splitting ipx into a directory service, a backend and a frontend (see the tracking issue).

The frontend becomes its own package and container, deployed without rebuilding Rust.

  • web/ gets its own package.json and builds dist/; build.rs and the include_str! pages leave the backend.
  • New image ipx-web: Caddy serves dist/ and proxies /api, /api/events (SSE) and /media to ipx:8099. One origin, so cookies, SSE and Cloudflare Access keep working.
  • The Cloudflare tunnel for ipodderx.sdf1.net points at Caddy instead of ipx. trusted_proxies takes Caddy's address, and Caddy passes the Cf-Access-* headers through; the Access JWT is still checked.
  • Hashed app.js/app.css names stay, served immutable; the pages no-cache.
  • Without the server's redirect, the page goes to /login itself on a 401.
  • The theme is put on the page on the server today (with_theme); it moves to the client, read before first paint, so there is no flash of the wrong theme.
  • admin.html becomes public markup. Every admin action is already checked in its handler (403), so this exposes layout, not access.
  • The Playwright suite starts Caddy too, so it tests what production runs.
Part of splitting ipx into a directory service, a backend and a frontend (see the tracking issue). The frontend becomes its own package and container, deployed without rebuilding Rust. * `web/` gets its own package.json and builds `dist/`; `build.rs` and the `include_str!` pages leave the backend. * New image `ipx-web`: Caddy serves `dist/` and proxies `/api`, `/api/events` (SSE) and `/media` to `ipx:8099`. One origin, so cookies, SSE and Cloudflare Access keep working. * The Cloudflare tunnel for ipodderx.sdf1.net points at Caddy instead of ipx. `trusted_proxies` takes Caddy's address, and Caddy passes the `Cf-Access-*` headers through; the Access JWT is still checked. * Hashed `app.js`/`app.css` names stay, served `immutable`; the pages `no-cache`. * Without the server's redirect, the page goes to `/login` itself on a 401. * The theme is put on the page on the server today (`with_theme`); it moves to the client, read before first paint, so there is no flash of the wrong theme. * `admin.html` becomes public markup. Every admin action is already checked in its handler (403), so this exposes layout, not access. * The Playwright suite starts Caddy too, so it tests what production runs.
rays added the enhancement label 2026-10-11 20:01:38 -07:00
Author
Owner

Built on the branch frontend-container (de6b0a4), not merged or deployed: the production switch changes how sign-in through the tunnel is trusted, so it waits for your go-ahead.

What it does:

  • web/build.mjs builds a static site, web/dist; Dockerfile.web serves it with Caddy (web/Caddyfile), passing /api and /media to ipx on the same origin. ipx serves no page: build.rs and the include_str!s are gone, and ipx's image builds without node.
  • Kept as it was: Caddy asks ipx before serving a page (forward_auth): /api/page-check (signed out → /login; not an admin → away from /admin, 403 for /admin.js), /api/login-check (Access-vouched → past the form), /api/signin for ?token= links.
  • Per-person page edits move into the page: web/boot.js in <head> sets the theme from its cookie and an admin's link from the last visit before the first frame; /api/me gives the version and the account's theme from before. A non-admin's page now has the Admin link in it, hidden; it gives nothing away, since the page and API are what guard it.
  • Caddy strips Cf-Access-* headers from every peer but TUNNEL_PEER.
  • Playwright runs through Caddy (72 pass); cargo test, tsc, smoke, native bridge, contrast pass; both images build, and a scratch run of the two together signed in, served the pages and refused a forged Access header.

Cutover plan (about a minute of downtime, nothing changes at Cloudflare):

  1. Build and push ipx and ipx-web.
  2. In /mnt/fast/arcane/projects/ipx/compose.yaml: add ipx-web (ports: 8099:8080, pinned ipv4_address: 192.168.64.10 on the default network, IPX_UPSTREAM: ipx:8099, TUNNEL_PEER: 192.168.64.1), and drop ipx's 8099:8099 so the LAN address and the tunnel reach Caddy.
  3. trusted_proxies in config.toml: 192.168.64.1 → 192.168.64.10 (Caddy).
  4. Pull and up -d ipx ipx-web; check the LAN address, a private window through ipodderx.sdf1.net (Authentik, then signed in as you), and the phone.
    Rollback: restore the two files from *.bak-pre-ipx-web, up -d ipx.

Merging: this and directory-client (#205) both branch from main and touch web.rs and dialogs.ts; whichever goes second gets rebased.

Built on the branch `frontend-container` (de6b0a4), **not merged or deployed**: the production switch changes how sign-in through the tunnel is trusted, so it waits for your go-ahead. What it does: * `web/build.mjs` builds a static site, `web/dist`; `Dockerfile.web` serves it with Caddy (`web/Caddyfile`), passing `/api` and `/media` to ipx on the same origin. ipx serves no page: build.rs and the `include_str!`s are gone, and ipx's image builds without node. * Kept as it was: Caddy asks ipx before serving a page (`forward_auth`): `/api/page-check` (signed out → `/login`; not an admin → away from `/admin`, 403 for `/admin.js`), `/api/login-check` (Access-vouched → past the form), `/api/signin` for `?token=` links. * Per-person page edits move into the page: `web/boot.js` in `<head>` sets the theme from its cookie and an admin's link from the last visit before the first frame; `/api/me` gives the version and the account's theme from before. A non-admin's page now has the Admin link in it, hidden; it gives nothing away, since the page and API are what guard it. * Caddy strips `Cf-Access-*` headers from every peer but `TUNNEL_PEER`. * Playwright runs through Caddy (72 pass); cargo test, tsc, smoke, native bridge, contrast pass; both images build, and a scratch run of the two together signed in, served the pages and refused a forged Access header. Cutover plan (about a minute of downtime, nothing changes at Cloudflare): 1. Build and push `ipx` and `ipx-web`. 2. In `/mnt/fast/arcane/projects/ipx/compose.yaml`: add `ipx-web` (`ports: 8099:8080`, pinned `ipv4_address: 192.168.64.10` on the default network, `IPX_UPSTREAM: ipx:8099`, `TUNNEL_PEER: 192.168.64.1`), and drop ipx's `8099:8099` so the LAN address and the tunnel reach Caddy. 3. `trusted_proxies` in config.toml: `192.168.64.1` → `192.168.64.10` (Caddy). 4. Pull and `up -d ipx ipx-web`; check the LAN address, a private window through `ipodderx.sdf1.net` (Authentik, then signed in as you), and the phone. Rollback: restore the two files from `*.bak-pre-ipx-web`, `up -d ipx`. Merging: this and `directory-client` (#205) both branch from main and touch web.rs and dialogs.ts; whichever goes second gets rebased.
Author
Owner

Merged as f987093 (rebased onto #205) and cut over as planned: ipx-web on 192.168.64.10 publishes 8099, ipx publishes no web port, trusted_proxies is 192.168.64.10, backups as *.bak-pre-ipx-web. Checked: a signed-out browser goes to /login, the API token works through Caddy, the tunnel still sends to Access, and a forged Access token from cloudflared's network reaches ipx's check and is refused for its signature (from Tower itself Caddy strips it, as it should; #209). Docs and the prod-check skill in 3bab621.

Merged as f987093 (rebased onto #205) and cut over as planned: ipx-web on 192.168.64.10 publishes 8099, ipx publishes no web port, trusted_proxies is 192.168.64.10, backups as *.bak-pre-ipx-web. Checked: a signed-out browser goes to /login, the API token works through Caddy, the tunnel still sends to Access, and a forged Access token from cloudflared's network reaches ipx's check and is refused for its signature (from Tower itself Caddy strips it, as it should; #209). Docs and the prod-check skill in 3bab621.
rays closed this issue 2026-10-12 07:22:03 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipx#203