Reference
Database schema
AgentsVille stores everything in one MySQL 8.4 database. There are twenty-seven tables: four that belong to authentication, one that holds instance-wide settings, one that is seeded content, and twenty-one that hold what Players create.
Every table is utf8mb4 with the utf8mb4_unicode_ci collation. Timestamps are
datetime(3) — millisecond precision — and are written in UTC. Identifiers are
varchar(191) holding application-generated UUIDs; the database generates no
ids.
Naming
Columns are snake_case in SQL and camelCase in TypeScript. Kysely's
CamelCasePlugin sits on the runtime connection and translates in both
directions — identifiers on the way out, result keys on the way back — so
mcp_server.owner_id is mcpServer.ownerId in application code. The rename
that made the database snake_case throughout and that plugin are halves of one
decision; applying either alone breaks every query. See
ADR 0019.
This page names columns as SQL holds them.
Migrations
The schema is built by migrations in packages/db/migrations, applied in name
order by Kysely's Migrator: the initial schema, which creates the tables and
seeds the first Templates; the rename that made every column snake_case; and each
change since, such as the one that made Providers rows and added Levels.
None has a down. Rolling back means writing the migration that undoes it; a
generated down that has never been run is not a rollback plan. The migrator
takes a lock first, so two containers starting together cannot both run the same
CREATE TABLE, and it keeps its own bookkeeping tables alongside the ones
below. See ADR 0018 and
Scripts for pnpm db:migrate.
What cascades
Deleting a Player deletes everything they own. user is the root: session,
account, npc, conversation, mcp_server, skill, memory,
connector, player_achievement and player_hidden_skin all reference it with
ON DELETE CASCADE. The exception is custom_skin.created_by, which is
ON DELETE SET NULL: an uploaded Skin belongs to the Village, so it outlives
the Player who made it.
Deleting an NPC deletes what belongs to that NPC: conversation, npc_tool,
sandbox, conversation_participant, and the skill and memory rows scoped
to it. One reference is deliberately different — message.npc_id is
ON DELETE SET NULL, so deleting an NPC does not delete the Conversation a
Player had with it. The transcript survives; the attribution goes null.
Deleting a custom_skin deletes its custom_skin_file rows.
Deleting an MCP Server cascades to the connector row that backs it, which is
why revoking a Connector is implemented as deleting its MCP Server.
Two things do not cascade, and are cleaned up in application transactions
instead: an npc_tool grant naming an MCP Server or a Skill holds that id in a
plain ref column with no foreign key, so the routes that delete a Skill or an
MCP Server delete the matching grants in the same transaction.
Every foreign key is ON UPDATE CASCADE.
user
The Players. The first Player to register becomes the Operator.
The Invite Code is never stored here, or anywhere else against a Player. It is
checked at registration and then forgotten. See
Village and invites and
invite_code below.
skin and villager sit on user rather than in a preferences table because they are two
values with nowhere else to live yet; a preferences table can come with the
second. See
ADR 0031
and ADR 0033.
custom_skin
Uploaded Skins (ADR 0038).
Deleting a Skin removes its files, and wearers go back to Farm, or Classic if
they hid Farm: a user.skin that names no Skin reads as the default.
custom_skin_file
player_hidden_skin
invite_code
The Village's invite codes (ADR 0036).
Not linked to user: a code is never stored against a Player.
session
Sign-in sessions, written and read by better-auth.
account
Credentials, also better-auth's. For email-and-password registration this holds the password hash.
verification
Short-lived verification tokens, better-auth's third table.
village_setting
Instance-wide settings, one row per key. No foreign keys and no owner: these belong to the Village, not to a Player.
Keys in use:
The whole table is read as one object and cached, so a read costs nothing per key.
provider
Where models run. Seeded with anthropic and openai; the Operator adds more.
No owner: Providers belong to the Village.
See ADR 0022.
npc
The characters a Player creates.
provider and model move together: a model id only means something to the
Provider that issued it, so a half-set pair is not a state worth storing. Both
null is the one meaningful mixed case, and it means "inherit". See
NPC.
conversation
One Conversation between a Player and one or more NPCs.
npc_id is the founding NPC, not the membership list. Who is actually in a
Conversation is conversation_participant, and a Conversation has a participant
row from the moment it is created.
conversation_participant
Who is in a Conversation. One row is the ordinary case; two or more make it a Party.
(conversation_id, npc_id) is unique, so adding an NPC that is already present
is a no-op rather than a constraint error the interface has to interpret.
turn_order decides who speaks first when nobody is addressed by name, and it
is read ascending with added_at as the tie-break. A new participant is given
the count of participants already present, so the order is the order they were
added unless something changes it. See Party.
party_task
One item on a Party's task board.
message
One turn in a Conversation.
History is read ordered by created_at then id. The tie-break is not
decoration: created_at alone is non-deterministic for rows written in the same
millisecond, and a history that reorders itself between turns is a model that
contradicts itself.
parts is what makes thinking and Tool use visible rather than hidden. Because
MySQL hands json columns back already parsed, the CamelCasePlugin is
configured not to recurse into values — a Tool call stored here keeps its own
keys. See Conversation and parts and
Usage.
token_usage
The Usage ledger: one row per model call. No foreign keys, so deleting a Conversation, an NPC or a Player's Message never changes what was used; and no owner cascade, so a deleted Player's rows stay too.
Its migration backfilled one message row per earlier assistant Message with
tokens, taking the provider and model from the NPC as it was then.
npc_group
A named set of one Player's NPCs.
npc_group_member
tool_grant
Grants that reach many NPCs: every NPC the Player has (all) or a Group's
members. Per-NPC grants stay in npc_tool.
A unique index cannot dedupe rows whose group_id is NULL, so each set is
replaced in one transaction instead. What an NPC can use is its own npc_tool
rows, plus these, minus its own rows with enabled = false.
host_mount
Host folders the Operator lends to Sandboxes.
npc_tool
Which Tools one NPC may use. One row per grant.
(npc_id, kind, ref) is unique. ref carries no foreign key, because it points
at three different things depending on kind — which is why deleting a Skill or
an MCP Server also deletes the grants naming it, in the same transaction.
Two builtin refs are grants rather than single Tools: sandbox turns on the
Sandbox Tools, and memory turns on the Memory Tools. See
Tools.
skill
Written instructions an NPC loads on demand.
A null npc_id is the whole point of the column being nullable: it means the
Skill belongs to the Village and every NPC in it can be granted the Skill. A
non-null npc_id scopes it to that one NPC, and the Skill is deleted with it.
Listing Skills for an NPC returns both. See Skills.
mcp_server
An external process or endpoint offering Tools over the Model Context Protocol. Registered once, enabled per NPC.
headers is where a bearer token for a remote server lives. Reads report only
whether it is set, never a masked value: a field that is sometimes a secret and
sometimes a placeholder is a field someone will eventually save back. See
MCP servers.
connector
A pre-configured MCP Server plus the credentials to reach a third-party service.
mcp_server_id being unique is what makes a Connector exactly one MCP Server: a
Connector is not a separate mechanism, it is a Tool source with an OAuth story.
Because the reference cascades, deleting the MCP Server removes the Connector
with it. The routes that list Connectors name their columns explicitly so the
two token columns cannot be selected by accident.
A pending OAuth flow is not stored here. It lives in Valkey for ten minutes and expires without being asked. See Connectors.
sandbox
The record that an NPC has been granted a machine, and on what terms.
One Sandbox per NPC, so npc_id is the key rather than a column beside one.
The row is the record of the decision; the container is the consequence. The row
is read every time a machine is started, which is what stops a restarted broker
quietly bringing a Sandbox back on the wrong network. Revoking a Sandbox deletes
the row and destroys the machine, filesystem and all. Only the Operator can set
egress. See Sandbox.
memory
Something an NPC should still know in a Conversation that has not happened yet.
scope decides who the Memory is for. npc means it belongs to one NPC;
village means every NPC in the instance shares it. Listing an NPC's Memories
returns both, because a list that omitted the shared ones would misrepresent
what the NPC knows.
embedding is a JSON array rather than a vector column, because MySQL 8.4 has
no vector type. Recall prefilters with the FULLTEXT index on content and
scores the survivors in process. Editing a Memory clears the embedding rather
than recomputing it, so an edit stays instant and the Memory is still found by
keyword. See ADR 0010 and
Working with memory.
npc_template
Starting points for a new NPC. Read-only at runtime and seeded by migrations
(the initial schema and 20260928090000_more_templates).
No owner and no foreign keys: Templates belong to the instance, and a Template is copied on use rather than linked, so nothing points back at this table. The seeded set is Wren (Researcher), Pike (Engineer), Quill (Editor), Ledger (Analyst), Compass (Generalist), Tally (Accountant), Lark (Writer), Lens (Reviewer) and Brief (Legal). See Templates.
player_achievement
The Achievements each Player holds.
A row is written the first time a Player meets an Achievement and never
removed, so deleting what earned it — the NPC behind "First NPC", say — does
not take the Achievement away. XP and Rank are not stored: both are derived
from these rows every time they are read, by summing the xp each held
achievement_id is worth in the catalogue and applying the Rank formula to
the total. See
Achievements and Rank and
ADR 0032.