Skip to content

Support deploying backend and viewer on separate hosts - #86

Merged
stolau merged 1 commit into
mainfrom
separate-host-deployment
Jul 27, 2026
Merged

Support deploying backend and viewer on separate hosts#86
stolau merged 1 commit into
mainfrom
separate-host-deployment

Conversation

@stolau

@stolau stolau commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

Backend and viewer were only deployable together, same host, via docker-compose.yml's nginx
same-origin proxy. Cleaning up the self-hosting story now that a Cloud offering is being built
elsewhere -- this repo's self-hosted path should stand on its own, including for operators who
want backend and viewer on separate hosts/domains.

Found two real, pre-existing bugs while investigating (not introduced by this change) that make
"separate hosts + auth token" -- the combination anyone hosting a real, internet-reachable backend
would want -- silently broken:

  1. CORS preflight never allowed Authorization or PATCH. before_request already exempts
    OPTIONS from the token check, so preflight always succeeded -- it was specifically the
    preflight response's allow-lists that silently broke the real follow-up request. Fixed;
    only adds to an already origin-gated allow-list, not a loosening of who gets CORS headers.
  2. viewer/Dockerfile never parameterized VITE_API_BASE, so a viewer image could never be
    built pointed at a separate-origin backend. Added ARG VITE_API_BASE="", default empty so
    docker-compose.yml's existing behavior is byte-for-byte unchanged. Found a third gap while
    verifying this for real: nginx.conf's /api/ proxy hardcodes backend:5000 as its upstream
    and nginx refuses to even start if that's unresolvable (true outside the compose network) --
    now dropped at build time when VITE_API_BASE is set, since that block is dead weight then.

docs/SELF_HOSTING.md consolidates the env var reference (previously scattered across 4 files)
and documents both topologies.

Test plan

  • Architect-reviewed design (round 1 CONCERNS -- missed the PATCH-method sibling bug in the
    same CORS block -- folded in)
  • Both CORS fixes mutation-tested -- 142/142 backend tests (up from 141)
  • Dockerfile fix verified for real: built the viewer with and without --build-arg VITE_API_BASE, confirmed the default case's nginx config is byte-for-byte unchanged and the
    override case's /api/ block is cleanly removed
  • Full separate-host topology run end-to-end with Podman (backend :5099, viewer :8099,
    genuinely separate origins, real PPS_ALLOWED_ORIGIN + PPS_API_TOKEN): unauthenticated
    request fails, authenticated cross-origin GET succeeds, two real cross-origin PATCH
    requests (category tag + rename) succeed live in a real browser
  • Full verification stack torn down completely afterward (containers, volumes, images) --
    via Podman, unlike every Docker attempt this session
  • npm run lint --workspaces clean
  • npm test --workspaces -- 3 client + 55 viewer, unaffected
  • npm run test:e2e -w examples/demo-app -- 8/8
  • backend ruff check + pytest -- 142/142
  • README.md, backend/CODEMAP.md, viewer/CODEMAP.md updated

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01NrQeh9CXrigN1cvpJuyzy4

…oss-origin auth bug

Backend and viewer were only deployable together, same host, via
docker-compose.yml's nginx same-origin proxy -- the documented and only
working path. Motivated by cleaning up the self-hosting story now that
a Cloud offering is being built in a separate private repo: this repo's
self-hosted path should stand on its own, including for operators who
want backend and viewer on different hosts/domains.

Found two real, pre-existing bugs while investigating (not introduced
by this change) that make "separate hosts + auth token" -- the
combination anyone hosting a real, internet-reachable backend would
want -- silently broken:

- backend/app/__init__.py's CORS preflight response never allowed the
  Authorization header, and never allowed the PATCH method (category
  rename, snapshot category update -- real routes the viewer calls).
  before_request already correctly exempts OPTIONS from the token
  check, so preflight itself always succeeded -- it was specifically
  the preflight *response*'s allow-lists that silently broke the
  follow-up authenticated request. Both are now included; the fix only
  adds to an already origin-gated allow-list (unchanged
  `if origin in allowed_origins` gate), not a loosening of who gets
  CORS headers at all.
- viewer/Dockerfile never parameterized VITE_API_BASE, so a viewer
  image could never be built pointed at a backend on a different
  origin. Added `ARG VITE_API_BASE=""`, threaded into the build step,
  defaulting empty so today's docker-compose.yml behavior is
  byte-for-byte unchanged. Discovered a second real gap while verifying
  this for real: nginx.conf's `/api/` proxy hardcodes `backend:5000` as
  its upstream, and nginx refuses to even *start* if that hostname
  isn't resolvable -- true for any deployment outside the compose
  network. Since that location block is dead weight once VITE_API_BASE
  is set (the built JS calls the absolute URL directly), it's now
  dropped at build time in that case; the default case keeps it
  unchanged.

docs/SELF_HOSTING.md consolidates the env var reference (previously
scattered across 4 files) and documents both topologies -- same-host
(cross-referencing the README, not duplicating it) and the new
separate-host path.

Verification: architect-reviewed design (CONCERNS round 1 -- missed the
PATCH-method sibling bug in the same CORS block -- fixed, folded in);
both CORS fixes mutation-tested against the real backend suite
(142/142, up from 141); the Dockerfile fix verified for real, not just
read -- built the actual viewer image with and without --build-arg
VITE_API_BASE, confirmed the default case's nginx config is
byte-for-byte identical and the override case's /api/ block is cleanly
removed; then ran the full separate-host topology end-to-end with
Podman (backend on :5099, viewer on :8099, genuinely separate origins,
real PPS_ALLOWED_ORIGIN + PPS_API_TOKEN): confirmed an unauthenticated
request fails, an authenticated cross-origin GET succeeds, and two real
cross-origin PATCH requests (category tag + category rename) succeed
live in a real browser -- proving the CORS fix, not just asserting it.
Full verification stack torn down completely afterward via Podman
(containers, volumes, images), something this session was never able
to do with Docker.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrQeh9CXrigN1cvpJuyzy4
@stolau
stolau merged commit 1dbe126 into main Jul 27, 2026
5 checks passed
stolau pushed a commit that referenced this pull request Jul 27, 2026
…flict

Three PRs landed on main after this branch forked (#86 self-hosting,
#87 mask popup, #88 six more e2e stories), two of which touched the
same examples/demo-app/CODEMAP.md paragraph this branch also edited.
index.html and style.css merged cleanly with no conflict; CODEMAP.md's
prose needed manual reconciliation -- kept main's up-to-date "fourteen
tests" description and spliced this branch's .thumbs gallery sentence
back into the site/ bullet it belongs to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrQeh9CXrigN1cvpJuyzy4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant