A loopback-only proxy for prototyping northway's feed reader A developer built a loopback-only Node.js proxy to prototype the reader interface for northway, a Go service that generates ranked, source-backed news feeds for AI agents. The proxy keeps the upstream API key server-side, enforces loopback binding with no override, and validates Host headers, Content-Type, Sec-Fetch-Site, and a 16 KB body limit before forwarding paid AI-provider requests. The prototype is intended strictly as a local UX reference, not a production component. Ahnii northway https://github.com/jonesrussell/northway is a Go service that turns approved sources into small, ranked, source-backed news feeds for AI agents, deployed Pi-first. Before committing that UX to the single-process Go build, I prototyped it in the browser first โ€” a plain HTML/CSS/JS reader backed by a small Node.js proxy. What follows is why that prototype needed its own proxy, and the checks that keep it from becoming anything more than a local UX reference. The prototype had one job: settle the reader's interaction design before any of it went into the Go service. That meant nailing down: Doing that in a browser means calling northway's real API from client-side code, and that creates an immediate problem: the API key can't go anywhere client-side JavaScript can read it. The fix is a small node:http server prototype/server.mjs that serves the static files and exposes one endpoint, /api/news , which holds the key and forwards requests upstream: js const apiKey = process.env.NORTHWAY API KEY; js const upstream = await fetch ${northwayURL}/v1/feed-queries , { method: "POST", signal: AbortSignal.timeout 10 000 , headers: { Authorization: Bearer ${apiKey} , "Content-Type": "application/json", "Idempotency-Key": randomUUID , }, body: JSON.stringify { feed id: selected.id, context: { intent: selected.context, technologies: , focus: selected.label }, max age hours: maxAgeHours, limit: 10, } , } ; The browser only ever talks to /api/news on the same origin. It never sees the key, the upstream URL, or the feed ID mapping โ€” those live entirely on the proxy side. The Idempotency-Key and a 10-second AbortSignal.timeout guard against duplicate or hung upstream calls, which matters when every query is a paid AI-provider request. A proxy that holds a live API key is a liability the moment it's reachable from anything but the machine running it. The server checks its own bind address before it does anything else: js const host = process.env.HOST ?? "127.0.0.1"; if "127.0.0.1", "localhost", "::1" .includes host { throw new Error "HOST must be a loopback address; this prototype cannot be exposed." ; } There's no flag to override this โ€” the only way past the check is to not pass a non-loopback HOST in the first place. The README spells out the same constraint in plain language: keep it bound to loopback, don't put it on a LAN, don't expose it to the internet. Loopback-only isn't a substitute for validating what shows up on /api/news . The handler rejects anything that doesn't look like the reader's own frontend before routing even happens: | Check | Rejects when | Response | |---|---|---| | Host header | Doesn't match 127.0.0.1: