The WebSocket origin check compared the browser's Origin host, which carries the port, against configured domains, which do not. publicOrigin only ever matches domains[0], so every other domain fell through to that comparison and was denied purely on the port: web.localhost:3000 never matched web.localhost. The result was a 403 on the HMR upgrade and a client reconnecting forever, while the page itself loaded fine because HTTP routing resolves the Host separately. Compares hostnames now. Unrelated and lookalike-suffix origins are still denied, and both cases are covered by tests. Verified through a real gateway: the HMR socket opens on both localhost and web.localhost, and a live edit reaches the browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>