Support deploying backend and viewer on separate hosts - #86
Merged
Conversation
…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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Backend and viewer were only deployable together, same host, via
docker-compose.yml's nginxsame-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:
AuthorizationorPATCH.before_requestalready exemptsOPTIONSfrom the token check, so preflight always succeeded -- it was specifically thepreflight 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.
viewer/Dockerfilenever parameterizedVITE_API_BASE, so a viewer image could never bebuilt pointed at a separate-origin backend. Added
ARG VITE_API_BASE="", default empty sodocker-compose.yml's existing behavior is byte-for-byte unchanged. Found a third gap whileverifying this for real:
nginx.conf's/api/proxy hardcodesbackend:5000as its upstreamand nginx refuses to even start if that's unresolvable (true outside the compose network) --
now dropped at build time when
VITE_API_BASEis set, since that block is dead weight then.docs/SELF_HOSTING.mdconsolidates the env var reference (previously scattered across 4 files)and documents both topologies.
Test plan
PATCH-method sibling bug in thesame CORS block -- folded in)
--build-arg VITE_API_BASE, confirmed the default case's nginx config is byte-for-byte unchanged and theoverride case's
/api/block is cleanly removed:5099, viewer:8099,genuinely separate origins, real
PPS_ALLOWED_ORIGIN+PPS_API_TOKEN): unauthenticatedrequest fails, authenticated cross-origin GET succeeds, two real cross-origin
PATCHrequests (category tag + rename) succeed live in a real browser
via Podman, unlike every Docker attempt this session
npm run lint --workspacescleannpm test --workspaces-- 3 client + 55 viewer, unaffectednpm run test:e2e -w examples/demo-app-- 8/8ruff check+pytest-- 142/142README.md,backend/CODEMAP.md,viewer/CODEMAP.mdupdatedCo-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01NrQeh9CXrigN1cvpJuyzy4