{"slug": "a-loopback-only-proxy-for-prototyping-northway-s-feed-reader", "title": "A loopback-only proxy for prototyping northway's feed reader", "summary": "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.", "body_md": "Ahnii!\n\n[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.\n\nThe prototype had one job: settle the reader's interaction design before any of it went into the Go service. That meant nailing down:\n\nDoing 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.\n\nThe 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:\n\n``` js\nconst apiKey = process.env.NORTHWAY_API_KEY;\njs\nconst upstream = await fetch(`${northwayURL}/v1/feed-queries`, {\n  method: \"POST\",\n  signal: AbortSignal.timeout(10_000),\n  headers: {\n    Authorization: `Bearer ${apiKey}`,\n    \"Content-Type\": \"application/json\",\n    \"Idempotency-Key\": randomUUID(),\n  },\n  body: JSON.stringify({\n    feed_id: selected.id,\n    context: { intent: selected.context, technologies: [], focus: [selected.label] },\n    max_age_hours: maxAgeHours,\n    limit: 10,\n  }),\n});\n```\n\nThe 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.\n\nA 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:\n\n``` js\nconst host = process.env.HOST ?? \"127.0.0.1\";\nif (![\"127.0.0.1\", \"localhost\", \"::1\"].includes(host)) {\n  throw new Error(\"HOST must be a loopback address; this prototype cannot be exposed.\");\n}\n```\n\nThere'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.\n\nLoopback-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:\n\n| Check | Rejects when | Response | \n|---|---|---|\n| Host header | Doesn't match `127.0.0.1:<port>` ,`localhost:<port>` , or`[::1]:<port>` | 400 | \n| Content-Type | Anything other than `application/json` | 415 | \n| `Sec-Fetch-Site` | Present and not `same-origin` (blocks other tabs and pages) | 403 | \n| Body size | Over **16 KB** | Aborted, never buffered | \n\nEvery response also carries a restrictive `Content-Security-Policy` (`default-src 'self'`, locked-down `script-src`/` style-src`, no inline scripts or styles), `X-Content-Type-Options: nosniff`, and `Referrer-Policy: no-referrer`. None of it is exotic, but skipping any row in that table turns \"only my machine can reach this\" into \"anything on my machine can reach this.\"\n\nOn the client side, a failed refresh shouldn't blank out a working feed. `app.js` tracks whether a snapshot has ever loaded successfully and falls back to it on error instead of clearing the screen:\n\n```\n} catch (error) {\n  if (requestNumber !== activeRequest) return;\n  if (error.name === \"AbortError\") return;\n  activeFeed = displayedFeed;\n  selectFeed(displayedFeed);\n  briefingHeading.textContent = feedLabels[displayedFeed];\n  briefingMeta.textContent = hasSnapshot\n    ? `Refresh failed · showing last available ${feedLabels[displayedFeed]} feed`\n    : \"Service unavailable\";\n  ...\n}\n```\n\nAn `activeRequest` counter also discards any response that isn't from the most recent fetch, so clicking between tabs quickly can't let a slow, stale response overwrite a newer one. When a feed genuinely comes back empty, the reader says so directly (\"No current stories matched this feed\") rather than padding the list. That's the same rule spelled out in `app.js` itself: the empty result is preserved rather than padded.\n\nThis prototype has clear limits:\n\nIts only job was to prove out the interaction design against real, live snapshots (all five feeds, desktop and mobile), so the accepted UX could guide a single-process Go implementation instead of being designed twice.\n\nBaamaapii", "url": "https://wpnews.pro/news/a-loopback-only-proxy-for-prototyping-northway-s-feed-reader", "canonical_source": "https://dev.to/jonesrussell/a-loopback-only-proxy-for-prototyping-northways-feed-reader-10ni", "published_at": "2026-09-14 12:21:47+00:00", "updated_at": "2026-09-14 12:38:54.540240+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-infrastructure"], "entities": ["northway", "Go", "Node.js", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/a-loopback-only-proxy-for-prototyping-northway-s-feed-reader", "markdown": "https://wpnews.pro/news/a-loopback-only-proxy-for-prototyping-northway-s-feed-reader.md", "text": "https://wpnews.pro/news/a-loopback-only-proxy-for-prototyping-northway-s-feed-reader.txt", "jsonld": "https://wpnews.pro/news/a-loopback-only-proxy-for-prototyping-northway-s-feed-reader.jsonld"}}