# Understanding the AgentMemory Real-Time Viewer Architecture

> Explore the AgentMemory real-time viewer architecture. Discover its lightweight Node.js design combining a static file server, CSP injection, and reverse proxy for efficient real-time data access.

- Repository: [Rohit Ghumare/agentmemory](https://github.com/rohitg00/agentmemory)
- Tags: architecture
- Published: 2026-05-10

---

**The AgentMemory real-time viewer is a lightweight HTTP front-end that combines a static file server, dynamic Content Security Policy injection, and a reverse proxy into a single Node.js process.**

The `rohitg00/agentmemory` repository provides you with a real-time viewer that runs alongside the core AgentMemory service. This architecture eliminates cross-origin issues by exposing the full REST API through the same host and port as the web interface, making client-side development seamless.

## Core Components of the Viewer

The viewer logic is completely isolated under `src/viewer`, allowing you to start or omit it without impacting the backend service. It consists of three tightly coupled subsystems.

### HTTP Server Layer ([`src/viewer/server.ts`](https://github.com/rohitg00/agentmemory/blob/main/src/viewer/server.ts))

The entry point creates a raw Node.js `http` server using `createServer`. This server handles three distinct responsibilities:

- **Static UI delivery**: Serves the viewer interface on the routes `/`, `/viewer`, and `/agentmemory/viewer`
- **CORS handling**: Applies a configurable allow-list via the `VIEWER_ALLOWED_ORIGINS` environment variable
- **Request routing**: Proxies all other requests to the AgentMemory REST API

By default, the viewer assumes the REST API runs on `viewerPort - 2`. If you start the viewer on port `3111`, it automatically proxies API calls to port `3109`.

### HTML Rendering and Security ([`src/viewer/document.ts`](https://github.com/rohitg00/agentmemory/blob/main/src/viewer/document.ts))

When you request the viewer UI, the server does not serve a raw static file. Instead, `loadViewerTemplate` reads [`viewer/index.html`](https://github.com/rohitg00/agentmemory/blob/main/viewer/index.html) and injects dynamic security tokens:

1. **Per-request nonce**: Generated via `createViewerNonce` to prevent XSS attacks
2. **Version metadata**: Injected from `VERSION` constant in [`src/version.ts`](https://github.com/rohitg00/agentmemory/blob/main/src/version.ts)
3. **CSP header**: Built by `buildViewerCsp` to restrict script execution to resources matching the generated nonce

This ensures that even if the HTML template contains placeholders, only scripts with the correct nonce can execute during that specific request.

### API Proxy Layer ([`src/viewer/server.ts`](https://github.com/rohitg00/agentmemory/blob/main/src/viewer/server.ts))

The `proxyToRestApi` function acts as a thin reverse proxy with these specific behaviors:

- **Path rewriting**: Prepends `/agentmemory/...` to incoming paths before forwarding
- **Method preservation**: Forwards the original HTTP method and body for `POST`, `PUT`, `DELETE`, and `PATCH` requests
- **Header passthrough**: Carries over the `Authorization` bearer token and other headers
- **Timeout protection**: Uses an `AbortController` with a **10-second timeout** to prevent hanging upstream calls
- **Response streaming**: Returns the upstream status, body, and content-type while preserving CORS headers

## Bootstrap and Configuration

You initialize the viewer using the `startViewerServer` function exported from [`src/viewer/server.ts`](https://github.com/rohitg00/agentmemory/blob/main/src/viewer/server.ts). The signature accepts the AgentMemory KV store, SDK instance, and optional authentication parameters.

```typescript
import { startViewerServer } from "./src/viewer/server.js";

// Start viewer on 3111, REST API expected on 3109
const viewer = startViewerServer(
  3111,              // viewer port
  kvInstance,        // AgentMemory KV store
  sdkInstance,       // iii-SDK instance
  process.env.VIEWER_SECRET, // optional API auth secret
);

```

Once running, you can access the UI and proxy API calls through the same origin:

```typescript
// Fetch the viewer HTML (with injected nonce)
const html = await fetch("http://localhost:3111/").then(r => r.text());

// Proxy a request to the underlying REST API
await fetch("http://localhost:3111/agentmemory/remember", {
  method: "POST",
  headers: { 
    "Content-Type": "application/json", 
    "Authorization": "Bearer SECRET" 
  },
  body: JSON.stringify({ content: "Important memory" }),
});

```

## Security Implementation Details

The security model relies on **nonce-based CSP** rather than complex authentication flows. The `buildViewerCsp` function, referenced through utilities in [`src/auth.js`](https://github.com/rohitg00/agentmemory/blob/main/src/auth.js), constructs headers that tie the injected nonce to allowed script sources. This approach lets the viewer safely load dynamic scripts while mitigating injection attacks.

The optional `VIEWER_SECRET` parameter enables basic bearer token validation. When provided, the proxy layer validates the `Authorization` header before forwarding requests to the REST API.

## Summary

- **Single-process architecture**: The viewer runs as one Node.js process alongside AgentMemory, not a separate service
- **Three-layer design**: HTTP server ([`server.ts`](https://github.com/rohitg00/agentmemory/blob/main/server.ts)), document generator ([`document.ts`](https://github.com/rohitg00/agentmemory/blob/main/document.ts)), and proxy handler (`proxyToRestApi`)
- **Zero CORS configuration**: By proxying API calls through the same port, you eliminate cross-origin browser restrictions
- **Request-scoped security**: Each page load generates a unique nonce via `createViewerNonce` and enforces it through `buildViewerCsp`
- **Configurable ports**: Default REST port is automatically calculated as `viewerPort - 2`, customizable via the `restPort` parameter

## Frequently Asked Questions

### How does the real-time viewer determine the REST API port?

By default, the viewer assumes the AgentMemory REST API runs exactly two ports below the viewer port. If you start the viewer on port `3111`, it proxies API requests to `3109`. You can override this by passing a custom `restPort` argument to `startViewerServer`.

### Is the Content Security Policy static or dynamic?

The CSP is **dynamic and request-scoped**. For every incoming request to the viewer routes, [`document.ts`](https://github.com/rohitg00/agentmemory/blob/main/document.ts) generates a fresh nonce via `createViewerNonce` and constructs a new CSP header via `buildViewerCsp` that only permits scripts matching that specific nonce.

### Can I disable the viewer and run only the core AgentMemory service?

Yes. All viewer code lives under `src/viewer` and is completely isolated from core memory functions. You can omit calling `startViewerServer` entirely without affecting the underlying `remember`, `recall`, or `forget` operations.

### What happens if the REST API takes longer than 10 seconds to respond?

The `proxyToRestApi` implementation uses an `AbortController` with a 10-second timeout. If the upstream AgentMemory API does not respond within this window, the proxy aborts the request and returns an error to the client, preventing the viewer from hanging indefinitely.