Summary
When the viewer is fronted by a reverse proxy on the standard HTTP/HTTPS port (80/443), the dashboard renders but every panel shows the empty "first run" state ("0 sessions", "No sessions yet…", "0 memories", etc.) — even though the underlying KV has data and the proxied REST endpoints return that data correctly.
Root cause is in the viewer's port-detection logic: it hardcodes '3113' as the fallback REST port, so every browser-side fetch goes to <host>:3113 instead of the same origin the page was served from.
Our deployment context
We run agentmemory as a host-level systemd service on a small-production server. The viewer is intentionally loopback-bound (127.0.0.1:3113), and we expose it through our standard reverse-proxy convention:
- A tiny
socat bridge container forwards traffic from a non-loopback port (0.0.0.0:13113) to the host's 127.0.0.1:3113, since the proxy lives in a Docker bridge network that can't reach the host's loopback directly.
- Nginx Proxy Manager terminates
http://agentmemory.<internal-hostname>/ on port 80 and forwards to 172.18.0.1:13113 → socat → viewer.
This is the normal "self-hosted service behind a reverse proxy on a private network" pattern — it's how every other service on this host is reached. We expected it to "just work" with agentmemory too.
Reproduction
- Run
agentmemory on a host. The viewer listens on 127.0.0.1:3113.
- Put any reverse proxy in front (nginx, Caddy, Traefik, NPM) that terminates on port 80 (or 443) and forwards
/ → 127.0.0.1:3113.
- Visit
http://<proxy-hostname>/ in a browser.
Expected
The dashboard populates from the same origin's /agentmemory/* endpoints (which are already proxied alongside the HTML).
Actual
- The viewer HTML loads cleanly.
- The dashboard shows "First run → magical moment in 10 seconds / Seed sample data + prove semantic recall works".
- Every counter is 0. "Recent Sessions: No sessions yet."
- Browser dev-tools Network tab shows every
/agentmemory/* request hitting <proxy-hostname>:3113 — a port that isn't reachable from outside the host (loopback-bound).
Meanwhile, curl http://<proxy-hostname>/agentmemory/sessions (no explicit port, same path the JS should hit) returns the populated session list correctly. The proxy chain is fine; only the JS URL construction is wrong.
Root cause
src/viewer/index.html (around lines 927–930 in v0.9.6):
var viewerPort = params.get('port') || window.location.port || '3113';
var REST = window.location.protocol + '//' + window.location.hostname + ':' + viewerPort;
When the page is served on port 80 (or 443), window.location.port is the empty string '', the fallback kicks in to the literal '3113', and the constructed REST URL is http://<host>:3113 — a port not exposed through the proxy.
The WS_URL / wsPort construction a few lines below (String(parseInt(viewerPort) - 1)) has the same issue, though it's only relevant when live updates are turned on.
Workaround (for anyone hitting this before a fix lands)
Append ?port=80 (or ?port=443 for HTTPS) to the URL:
http://<proxy-hostname>/?port=80
That makes params.get('port') win in the precedence chain, and every fetch goes to the same origin the page was served from. Works, but requires either remembering the query string or having the proxy auto-redirect / to /?port=80.
Suggested direction
When params.get('port') is absent and window.location.port is empty, construct the REST base from window.location.origin (i.e. don't append an explicit port) rather than falling back to the literal '3113'. Same logic for the WebSocket URL.
This would make the viewer "just work" behind any reverse proxy on standard ports, which is by far the most common self-hosted deployment shape.
Happy to provide more detail on the deployment or test a candidate fix on our side. Not opening a PR yet — wanted to flag the design question first in case the upstream preference is a different approach (env var, explicit base-URL config, etc.).
Plugin version: @agentmemory/agentmemory@0.9.6
Summary
When the viewer is fronted by a reverse proxy on the standard HTTP/HTTPS port (80/443), the dashboard renders but every panel shows the empty "first run" state ("0 sessions", "No sessions yet…", "0 memories", etc.) — even though the underlying KV has data and the proxied REST endpoints return that data correctly.
Root cause is in the viewer's port-detection logic: it hardcodes
'3113'as the fallback REST port, so every browser-side fetch goes to<host>:3113instead of the same origin the page was served from.Our deployment context
We run
agentmemoryas a host-level systemd service on a small-production server. The viewer is intentionally loopback-bound (127.0.0.1:3113), and we expose it through our standard reverse-proxy convention:socatbridge container forwards traffic from a non-loopback port (0.0.0.0:13113) to the host's127.0.0.1:3113, since the proxy lives in a Docker bridge network that can't reach the host's loopback directly.http://agentmemory.<internal-hostname>/on port 80 and forwards to172.18.0.1:13113→ socat → viewer.This is the normal "self-hosted service behind a reverse proxy on a private network" pattern — it's how every other service on this host is reached. We expected it to "just work" with
agentmemorytoo.Reproduction
agentmemoryon a host. The viewer listens on127.0.0.1:3113./→127.0.0.1:3113.http://<proxy-hostname>/in a browser.Expected
The dashboard populates from the same origin's
/agentmemory/*endpoints (which are already proxied alongside the HTML).Actual
/agentmemory/*request hitting<proxy-hostname>:3113— a port that isn't reachable from outside the host (loopback-bound).Meanwhile,
curl http://<proxy-hostname>/agentmemory/sessions(no explicit port, same path the JS should hit) returns the populated session list correctly. The proxy chain is fine; only the JS URL construction is wrong.Root cause
src/viewer/index.html(around lines 927–930 in v0.9.6):When the page is served on port 80 (or 443),
window.location.portis the empty string'', the fallback kicks in to the literal'3113', and the constructedRESTURL ishttp://<host>:3113— a port not exposed through the proxy.The
WS_URL/wsPortconstruction a few lines below (String(parseInt(viewerPort) - 1)) has the same issue, though it's only relevant when live updates are turned on.Workaround (for anyone hitting this before a fix lands)
Append
?port=80(or?port=443for HTTPS) to the URL:That makes
params.get('port')win in the precedence chain, and every fetch goes to the same origin the page was served from. Works, but requires either remembering the query string or having the proxy auto-redirect/to/?port=80.Suggested direction
When
params.get('port')is absent andwindow.location.portis empty, construct the REST base fromwindow.location.origin(i.e. don't append an explicit port) rather than falling back to the literal'3113'. Same logic for the WebSocket URL.This would make the viewer "just work" behind any reverse proxy on standard ports, which is by far the most common self-hosted deployment shape.
Happy to provide more detail on the deployment or test a candidate fix on our side. Not opening a PR yet — wanted to flag the design question first in case the upstream preference is a different approach (env var, explicit base-URL config, etc.).
Plugin version:
@agentmemory/agentmemory@0.9.6