Architecture
Containers and packages
AgentsVille ships as a docker compose stack. This page lists what runs, what each
service is allowed to reach, and how the TypeScript packages behind it are split.
The services
Four services run with no profile selected: mysql, valkey, server and
web. Every one of them publishes on loopback only. On a public server that
means the ports answer host tooling and an SSH tunnel, and do not answer the
internet — which is not something a password should have to be the only defence
against.
valkey is not optional. VALKEY_URL is validated at boot and the API refuses
to start without it. The reasoning is in
ADR 0014: a rate limiter that
falls back to counting inside one process when its store is missing is not a
limiter that degrades, it is a limiter that lies, and it would lie precisely in
front of the Invite Code.
Profiles
Two profiles exist in the Compose file.
--profile caddy adds the bundled edge. Caddy terminates TLS for
PUBLIC_APP_HOST and PUBLIC_API_HOST and issues certificates with no
configuration, which is why it is bundled at all — HTTPS on both origins is
mandatory (ADR 0001).
--profile sandbox adds sandbox-broker. The internal agentsville_sandbox_net
network that Sandboxes without egress are attached to is not created by the
profile: no service joins it, so Compose never makes it, and the Operator
creates it once with docker network create --internal agentsville_sandbox_net
(see Enabling Sandboxes). A
deployment that does not want Sandboxes never runs the broker and never mounts
the Docker socket anywhere.
There is no nginx, apache or none profile. An Operator who already runs a
proxy simply leaves the caddy profile off: the application ports are on
loopback, and tested reference configurations for nginx, Apache and Caddy ship
under deploy/. AgentsVille does not detect what is in front of it and never should —
the Operator chooses, AgentsVille states the contract
(ADR 0008). That contract is short:
the API origin must not be buffered or compressed, forwarded headers must be
set, and both origins must be HTTPS. See
Reverse proxy.
What each service may reach
web talks to server and to nothing else. It has no database connection, no
cache connection and no Provider keys. Server-side rendering resolves sessions at
http://server:8080 over the Compose network, so a page load does not hairpin
back out through public DNS.
server talks to mysql, to valkey, to the Providers, to whatever MCP Servers
and Connectors a Player registered, and to sandbox-broker. It does not hold the
Docker socket.
sandbox-broker holds the Docker socket, publishes no ports, has no route
through the reverse proxy, and still requires a bearer token of at least sixteen
characters or it refuses to start. "Nothing else can reach it" is a property of a
deployment, not of the code, so the token is there as well.
A Sandbox reaches nothing. It sits on an internal network with no route out, and has no route to the database, the API or the broker. An Operator may grant one NPC egress, which moves that Sandbox to a network that has a route to the internet and nowhere else useful. See The sandbox boundary.
The packages
The repository is apps/* and packages/* under pnpm, on Node 24.
@agentsville/config is a development-only package holding the shared TypeScript and
lint configuration. Every package depends on it and it contributes
nothing at runtime, so it is left out above.
What the packages hold:
@agentsville/ai— the Provider registry, the streaming call, prompt assembly (identity, Skill index, Memory, Party roster), the Tool registry, the MCP client pool, the Connector catalogue and OAuth flow, and credential encryption.@agentsville/auth— better-auth, configured against Kysely, with the session cache and the Operator claim.@agentsville/cache— the Valkey client and the typed helpers over it.@agentsville/db— the Kysely instance, the hand-written schema types, and the migrations.@agentsville/sandbox— theSandboxDriverinterface, the Docker driver that runs inside the broker, and the HTTP client the API uses to reach it. It depends on nothing else in the repository, which is what lets it be the one package both sides of that boundary import.@agentsville/observability— wide events throughevlog, with the drain chosen by environment variable rather than by import (ADR 0004).@agentsville/ui— the shared interface primitives.
Where configuration is read
@agentsville/env is the root of the dependency graph and the only place in the
codebase that reads the filesystem for configuration. It loads the
repository-root .env by walking up from the caller until it finds the
repository marker, so no caller states its own depth — which matters because the API is
bundled to a different depth than it is authored at. Values already present in
the environment win, so Compose and shell overrides still apply. Nothing depends
on dotenv.
Two things deliberately sit outside that schema. Log drain vendor keys are read
by evlog from process.env directly, so adding a destination is never a schema
edit (ADR 0004). And the migration
runner reads DATABASE_URL from the environment itself rather than through
@agentsville/env/server, because that schema validates everything the API needs — now
including VALKEY_URL — and a migration has no business refusing to run because
a cache is unconfigured.
The full list is in Environment variables. The tables are in Database schema.