Architecture
Architecture overview
A Village is one instance of AgentsVille, run by one Operator, on hardware and Provider keys that Operator controls. This section describes what that instance is made of and where its boundaries fall. It is written for someone deciding whether to host AgentsVille, or auditing an instance that is already running.
What is in the request path
A Player reaches AgentsVille over HTTPS through a reverse proxy, which terminates TLS
for two hostnames: app.<domain> for the web application and api.<domain> for
the API. The proxy may be the bundled Caddy, or an nginx or Apache the Operator
already runs. Both hostnames must be HTTPS; there is no working plain-HTTP
deployment, including on a local network.
Behind the proxy:
webserves the application — server-rendered pages plus the browser bundle. It holds no Provider keys and has no database connection. It resolves sessions by callingserverover the internal network rather than through public DNS, so a page load does not depend on the TLS edge being reachable from inside the stack.serveris the API. It owns the session bridge, every domain route, the event streams that carry a Message as it is produced, and the orchestration of Tool calls. It holds the Provider keys, the database connection and the session secret.mysqlholds every persisted row: NPCs, Conversations, Messages and their Parts, Skills, Memory, MCP Servers, Connectors, Sandbox records and Village settings.valkeyholds the state that cannot live inside one process: rate-limit counters, pending Connector authorizations, and a cache in front of session and settings reads. It is required at boot.
From server, three things are reached outward:
- Providers — Anthropic and OpenAI — over the public internet, with the Operator's keys.
- MCP Servers and Connectors, which supply Tools. Some are processes on the host, some are HTTP endpoints somewhere else.
sandbox-broker, when Sandboxes are enabled. It is the only service that holds the Docker socket, and it creates oneagentsville-npc-<npcId>container per NPC. Those Sandboxes have no route back to the API, to the database or to the broker, and no route to the internet unless the Operator granted one.
The Operator does not appear in the request path at all. They hold the Provider
keys, edit the root .env, and set Village defaults. Everything else that happens
in a running instance is a Player action.
See Containers and packages for what each service is and how the code is split, and The sandbox boundary for why the broker exists.
What is deliberately simple
These are recorded so a reader knows they are choices rather than oversights. Each one has a stated ceiling, and most have a decision record behind them.
No shared types package. The web client hand-mirrors the API's response shapes rather than importing them. Introducing RPC or a shared schema package is a bigger change than the drift it prevents, at this size. The cost is named in ADR 0015: a row is sent to the browser as it comes out of the database, so a column renamed in a migration is an API change nobody had to approve.
No global state library on the web side. Router loaders fetch server data,
component state holds a stream in progress, and router.invalidate() reconciles
the two when a stream ends.
No roles. The Operator is whoever registered first, recorded as one boolean on their row and claimed atomically so a simultaneous double registration cannot produce two (ADR 0009). There are no groups, no permission sets, no delegation and no way to grant it from the interface. Three things are Operator-only: Village settings, registering a stdio MCP Server, and giving a Sandbox internet access.
No vector database. MySQL 8.4 has no vector type. Every Memory stores its
embedding as a JSON array; recall narrows with a FULLTEXT match on the content
and scores the survivors by cosine similarity in process
(ADR 0010). Recall is therefore lexical
before it is semantic, and an instance with no embedding model still remembers —
it simply ranks by keyword.
No per-vendor integrations. A Connector is an MCP Server with credentials and a friendly name, and nothing downstream of the row can tell one from a server a Player registered by hand (ADR 0011). Adding a service to the catalogue is a few lines of code and no migration.
No quota enforcement. Usage is recorded on every Message, including on the abort and error paths, and nothing in AgentsVille reads it to restrict anyone (ADR 0003). The real ceiling on spend is the cap set in the Provider's own console. Because nothing is stopped financially, loops are stopped structurally instead: a step limit per turn, a hop limit per Party turn, an idle timeout on MCP clients and an idle reaper on Sandboxes.
One Village per deployment. No tenancy and no cross-Village isolation. Everything a Player creates lives in exactly one Village, and Village-scoped settings and Memory are shared across every NPC inside it.
No rollback migrations. Migrations have no down step. Rolling one back
means writing the migration that undoes it, which on a single-server deployment
with a nightly dump is the honest answer rather than a missing feature
(ADR 0018).
Where to read the rest
- Containers and packages — what runs, what each service may reach, and how the code is split.
- A turn, end to end — what happens between a Player sending a Message and the reply being stored.
- Tool execution — where a Tool call goes and what bounds it.
- The sandbox boundary — the strongest isolation boundary in the system, and what it does not cover.
- Security posture — the controls, and the gaps that are still open.
- Decision records — why each of these is the way it is.
- Database schema — the tables themselves.
- Self-hosting — how to actually run it.