Self-hosting
Reverse proxy
AgentsVille does not detect what is in front of it and never should. You choose an edge; AgentsVille states what that edge has to do and ships a tested configuration for each of the three common ones.
The contract
Whatever terminates TLS for your Village must satisfy three things.
The API origin must not be buffered or compressed. A reply from an NPC streams Part by Part over Server-Sent Events. A proxy that buffers turns a live transcript into one burst at the end. It does not error and nothing logs it — it just feels broken. A proxy that compresses the stream usually buffers it in order to do so, which is the same failure by another route.
Forwarded headers must be set. Rate limits are keyed on the client address,
so without X-Forwarded-For every caller shares one bucket.
Both origins must be HTTPS. The session cookie is SameSite=None; Secure, so
a browser will not store it from a plain-HTTP origin. There is no plain-HTTP
deployment of AgentsVille, LAN included.
The web and API hosts must share a registrable domain. app.example.com and
api.example.com — as the bundled Caddyfile assumes — share the registrable
domain example.com; a proxy in front of two unrelated domains does not.
Splitting them across two different registrable domains breaks sign-in today,
in Safari, Firefox and Brave, and in the desktop app on macOS, because the
session cookie stops travelling between the two origins.
The reasoning is recorded in ADR 0008.
Bundled edge or your own
The bundled edge is a caddy service behind a Compose profile:
none is not a service — it is a profile name that matches nothing, which is
how you spell "start the base stack and no edge". The application ports bind to
127.0.0.1 either way, never to 0.0.0.0: an application port on a public
interface is an origin with no TLS and no forwarded headers, which is the
failure the contract exists to prevent.
pnpm docker:up, pnpm docker:build and pnpm docker:down are shorthands for
the caddy profile.
Caddy
deploy/caddy/Caddyfile is the reference implementation of the contract, and
the only option with nothing to do about certificates. Point both hostnames at
the host before the first up and Caddy issues and renews them.
It serves two site blocks, driven by PUBLIC_APP_HOST and PUBLIC_API_HOST:
- The web host gets
encode zstd gzipand the document security headers — HSTS,nosniff,X-Frame-Options: DENY, a referrer policy, and a Content-Security-Policy that ships as Report-Only. - The API host gets neither. There is deliberately no
encodedirective on it, andflush_interval -1forwards each chunk as it arrives. The API sets its own, far stricter headers in the application, because it never answers with a page.
TRUSTED_PROXIES can stay at its default of true here, because nothing but
Caddy can reach the application ports.
nginx
Copy deploy/nginx/agentsville.conf to /etc/nginx/sites-available/agentsville, replace
the two example hostnames and the certificate paths, and run AgentsVille without the
caddy profile. Certificates come from certbot:
The file defines two upstreams — 127.0.0.1:3000 for the web app and
127.0.0.1:8080 for the API — a port 80 block that redirects to HTTPS, and one
server block per origin.
Four directives in the API block are what keep the stream a stream:
proxy_buffering off, proxy_request_buffering off, proxy_cache off and
chunked_transfer_encoding on. Two more matter as much: proxy_read_timeout 1h
and proxy_send_timeout 1h, because a reply can take minutes of model time and
nginx's 60-second default cuts long generations off mid-sentence.
Do not add gzip on to the API server block. Compressing a stream means
holding it to compress it. The block sets gzip off explicitly for that reason,
while the web block keeps compression on — those are documents, not streams.
One thing worth knowing: the API server sends X-Accel-Buffering: no, and nginx
honours it even with proxy_buffering on. The directives in the shipped file
are belt and braces for a proxy that ignores the header. Compression is the
failure that header cannot save you from.
Apache
deploy/apache/agentsville.conf needs the modules enabled first:
Then the same substitutions: two hostnames, two certificate paths.
On the API virtual host, flushpackets=on forwards each chunk as it arrives
instead of filling a buffer first, and SetEnv proxy-sendchunked 1 keeps the
response chunked end to end. SetEnv no-gzip 1 and SetEnv dont-vary 1 keep it
from being compressed. timeout=3600 on the ProxyPass and ProxyTimeout 3600
are the long-generation equivalent of nginx's read timeout.
Your own proxy
Run without the caddy profile and point your proxy at 127.0.0.1:3000 and
127.0.0.1:8080. Satisfy the three rules above, then narrow
TRUSTED_PROXIES from its default to the addresses your proxy actually uses:
Leaving it at true trusts any upstream's X-Forwarded-For. On a reachable
port that hands a caller a fresh identity per request, including on the route
protecting the Invite Code.
Prove the edge does not buffer
Buffering is silent, so measure it rather than assuming it:
That runs scripts/sse-smoke.mjs against GET /api/stream-check — a fixed
five-frame stream, spaced 100 ms apart, that needs no account and no Provider
key, so you can test the edge before anyone has registered and without spending
a token.
It fails if the response is not an event stream, if the edge set a
Content-Encoding header at all, if fewer than two chunks arrive, or if every
chunk lands within 150 ms of the first. A passing run prints the chunk count,
first and last byte times, the spread between them, and PASS.
Run it after any change to the proxy configuration, and against each edge you try. It also works against the application directly, which is the way to tell an application problem from an edge problem:
Changing the domain later
The API hostname is compiled into the browser bundle at build time. Changing
PUBLIC_API_URL needs docker compose up -d --build, not a restart. See
Upgrading.
Next
- Troubleshooting — what a buffering edge
and a wrong
TRUSTED_PROXIESlook like from the outside. - Security posture — what the edge is and is not protecting.