Skip to content

The local API

A loopback-only server, a per-launch token, and one event stream.

The web UI and the backend are one app on one machine: no remote server, no CORS, and no way to reach the API from another device.

Loopback only, token always

Kestrel binds to 127.0.0.1 exclusively: an ephemeral port in production, a fixed one (47210) in dev. Every API request except the health check must carry a bearer token minted fresh at each launch; a Host-header guard additionally rejects DNS-rebinding tricks.

How the SPA gets the token differs by mode:

  • Production: the Host serves the built SPA itself and templates the token into index.html as it goes out.
  • Dev: the Host writes { port, token } to mnemo-web/.dev/api.json, and the Vite proxy injects the Authorization header on every proxied request.

The content security policy

SpaHosting.LoadTemplatedIndex (Mnemo.Host/Web/SpaHosting.cs) mints one nonce per launch, not per response. The policy sent with each response:

default-src 'self'
script-src 'self' 'nonce-<value>'
style-src 'self' 'unsafe-inline'
img-src 'self' data: blob:
font-src 'self' data:
media-src 'self' blob:
worker-src 'self'
connect-src 'self'
object-src 'none'
base-uri 'self'
form-action 'none'
frame-ancestors 'none'

A script without the current nonce is refused, which stops chat-rendered model output from executing. connect-src 'self' blocks fetch and XHR to any origin but the Host's own. Styles keep unsafe-inline, since editor components inject styles at runtime.

The event stream

Server-to-client push is one SSE channel, GET /api/events. The Host publishes typed events (toasts, shutdown) into a bounded per-subscriber channel that drops its oldest entries rather than blocking the publisher, and never replays to a client that connects later, so nothing may treat the stream as a source of truth. On the SPA, an event whose payload alone decides the effect gets a case in one dispatch switch; an event whose meaning depends on what a page currently has open gets no case and reaches that page through a subscriber instead. Chat streaming is separate: each assistant turn streams its own SSE response with deltas, tool calls, and status events.

Shutdown is a handshake

Closing the window does not kill the process mid-save: the first close request is held, a shutdown event goes out over the stream, and the SPA gets a short grace period to flush pending saves and reply that it is ready. Pressing close a second time overrides the wait.

Adding an endpoint

Endpoints are minimal-API handlers mapped feature by feature over services resolved from DI, living with their feature (for example Mnemo.Host/Flashcards). Copy any existing feature folder: contracts in, service call, DTO out, nothing clever in the route handler.