Skip to main content

Architecture

The Shatterveil server is built around seven standing rules. They were set before the first line of server code was written, and the build enforces as many of them as it can. If a rule and some code disagree, the code is wrong.

The full, canonical text is design/04-architecture-rules.md in the server repository. This page is the same content written for someone about to work on the server.

The seven rules at a glance​

  1. Everything is a module. Every game system can be switched on or off while the server runs.
  2. Shared code lives in libraries. Written once, reused. Modules never copy each other's code.
  3. Everything is documented. Javadoc on every public member; every command documents itself, including who may use it.
  4. Security first. Nothing a client sends is trusted. Client-side networking is only allowed in modules that aren't security-critical.
  5. Built for Hermes. The AI agent can help administrate, through a safe, controlled interface only.
  6. Built for the admin panel. The server exposes what a web panel will need without a later redesign.
  7. Log everything worthwhile. One structured event stream, readable on the web and in hidden staff Discord channels.

Plus one working rule for every repo: all work happens on develop; nothing reaches main without Preck's say-so.


Rule 1: Everything is a module​

A module is one game system — combat, chat, parties, quests — packaged so the server can start and stop it on its own. Each module:

  • lives in its own Gradle subproject, modules/<name>/;
  • has exactly one entry class implementing ShatterveilModule;
  • declares a manifest: id, name, version, description, the modules it depends on, its security class, and whether it's a kernel module;
  • registers everything it adds — event listeners, commands, scheduled tasks, published services — through the ModuleContext it is handed, never on the global Minestom managers.

Why the context matters​

Minestom can undo each kind of registration: an event node can be removed from its parent, a command unregistered, a task cancelled. The ModuleContext records every registration it makes, and disabling a module calls undoAll(), which reverses the whole list. A module therefore can't "forget" to clean up, and a disabled module has no listeners, commands, tasks or services left behind.

What the context gives a module:

CallWhat it does
events() / listen(type, handler)The module's own child EventNode, detached on disable
registerCommand(command, doc)Registers a command with its documentation (use the commands builder rather than calling this directly)
scheduleTask(...) / submitTask(...)Scheduled tasks, cancelled on disable
publish(type, service)Publishes a service in the ServiceRegistry, withdrawn on disable
services()Finds services other modules published (the only sanctioned way to talk to them)
log()A ModuleLog stamped with the module's id, for audit/game/system events
logger()An SLF4J logger for ordinary console output
onlinePlayers() / findPlayer(name)Read-only access to the player list

Lifecycle​

LOADED --enable()--> ENABLED --disable()--> DISABLED --enable()--> ENABLED ...
|
(error in enable) --> FAILED (logged, alerted, stays off)
  • onEnable(ctx): the module sets itself up through its context.
  • onDisable(ctx): optional. The context undoes all registrations regardless; the module uses this hook to save state it owns and, where it matters, to tell affected players in plain words ("Trading is temporarily unavailable").
  • Dependencies: a module can't be enabled while something it depends on is off. Disabling a module that others depend on requires --cascade, which turns those off too. The ModuleManager resolves dependency order and refuses cycles.
  • Kernel modules can't be disabled at runtime, because turning them off would make the server unsafe or unmanageable. Today those are the module manager itself (part of server/), permissions, logging, player-data and admin-api.
  • State survives restarts: which modules are enabled is stored in ClickHouse (module_state), or in data/modules.json when no database is configured.

Controls​

Modules can be toggled from three places, and all three are meant to go through the same admin API so the rules and the audit log are identical:

:::info Phase 0 status The admin API is read-only today, so /module talks to the ModuleManager directly. Write actions on the API are Tier 5 work. :::

When a module is done​

A module isn't finished until all of these hold:

  • Javadoc on every public class and method.
  • A README.md — and a page under Modules & libraries.
  • Every command documented through the commands library.
  • Tests that pass.
  • A toggle test: enable → disable → enable → disable works and nothing is left behind (ModuleTestSupport.assertToggleLeavesNothingBehind).

Rule 2: Shared code lives in libraries​

Shared code lives in library subprojects under libs/. The dependency direction is strict:

  • Modules depend on libraries.
  • Libraries never depend on modules.
  • Modules never depend on each other's code. They talk only through events and through services published in the ServiceRegistry.

The nine libraries that exist today are documented under Modules & libraries › Libraries. Three more (content, gui, display) are planned and will be built in the tier that first needs them.

Enforced by the build. Gradle's convention plugins refuse a module → module or library → module dependency at the project level, and an ArchUnit test in server/src/test refuses it at the package level. Code review watches for near-duplicate code: when two modules need the same function, it moves into a library in the same change.


Rule 3: Everything is documented​

  • Javadoc on every public class and every public or protected method: what it does, parameters, return value, exceptions, and whether it's thread-safe. The build runs Javadoc with -Xdoclint:all -Xwerror, so a missing or broken comment fails ./gradlew build.
  • Every module has a README.md covering what it does, its manifest and security class, settings, commands, events sent and received, and what happens to players when it's disabled.
  • Every command is documented where it's defined. The commands library requires a description, usage, examples and a permission; a command missing any of them throws at registration, so an undocumented command can't exist. The build renders these into docs/commands.md and fails when that file is stale — the Commands page here is the published copy.
  • Commands explain themselves in game. Wrong usage prints the same usage text as the docs.

Rule 4: Security first​

The stance​

  • The server is authoritative. The client says what the player tried to do (clicked, moved, typed). The server decides what happened (damage, drops, trades, quest progress, money).
  • Never trust client input. Every value from a client is checked for range, type, permission, rate and game state before anything uses it.
  • Least privilege. Modules, staff roles and API tokens each get only the permissions they need.

Security classes​

Every manifest declares one of two security classes.

CRITICALSTANDARD
Examplescombat, items, currency, trading, loot, quests, permissions, moderation, admin API, player datacosmetics, emotes, music, ambient particles, HUD style, companion skins
May it read custom client data (plugin-message channels, custom-click payloads, client-mod messages)?No. Only vanilla actions (click, move, chat, inventory click), each validated server-side.Yes, through the security library's guarded, rate-limited channel — and never to change anything in the CRITICAL list.
May it change player state other modules rely on?Yes, through its own validated logic.No. Only its own state.
ReviewEvery change needs a security note in the commit message.Normal review.

Enforced by the build. The ArchUnit test fails if a CRITICAL module uses PlayerPluginMessageEvent, PlayerCustomClickEvent or packet listeners directly. STANDARD modules must use GuardedChannel from the security library, which checks size, type and rate before handing data on.

Specific protections​

  • Inputs: every command argument, chat message, sign, book, anvil rename and GUI click is validated. GUI clicks are checked against the menu the server actually opened.
  • Rate limits: per player, on commands, chat, clicks, trades and spell casts.
  • Movement and combat: server-side reach, attack-speed, movement-speed and flight checks. Cheating is flagged and logged; automatic punishment comes later, once tuned.
  • Money and items: every change goes through one transaction function in the data library — atomic, logged, and unable to create or destroy value by accident. With ClickHouse this is an append-only ledger (see Database). This is what stops duplication exploits. (Planned: Tier 3, economy module.)
  • Secrets: database passwords and API tokens live in environment variables or a secrets file, never in git.
  • Dependencies: a vulnerability scanner watches library versions.

Rule 5: Built for Hermes​

Hermes (the project's AI agent) should be able to help administrate, but never have more power than it needs.

  • Through the admin API only. No console, database or file access to the game server, and it never runs server commands as raw text.
  • Its own service account, with its own token and the hermes role. That role includes only player permissions and can never manage roles — the PermissionTable refuses to grant it shatterveil.role.manage, even by table edit.
  • Planned as an MCP server: a small bridge publishes the admin API to Hermes as tools such as server_status, list_modules, toggle_module, find_player, read_reports and mute_player.
LevelExamplesRule
Readserver status, TPS, player count, module states, reports queue, audit log, player lookupAllowed
Act, low impacta short temporary mute, toggle a STANDARD module, broadcast a noticeAllowed, logged, and Preck is notified
Act, high impactban, toggle a CRITICAL module, change items or money, restart the serverNeeds Preck's approval, given in the admin panel or chat, before it runs
Neverchange permissions or roles, create API tokens, read private messages beyond a report's snapshot, read secretsNot possible with the hermes token at all

Every Hermes action lands in the audit log with who asked, why, and what changed — the same as actions by human staff.


Rule 6: Built for the admin panel​

The web admin panel isn't being built yet, but the server is designed so it can be added without redesigning anything.

  • One admin API is the single control surface. The in-game /module commands, Hermes, the Discord bot (mcrpg-discord) and the future web panel all call it. There is never a second way to do the same thing. The Discord bot gets its own token and role, like Hermes.
  • Where it runs: a small HTTP service on the server machine, listening on localhost only, never exposed to the internet. The panel will reach it through the website backend, behind a login.
  • Authentication: a separate scoped token per client. Tokens can be revoked. Every call carries the identity of the person or agent behind it.
  • Audit log: every change made through the API is stored with who, what, when, why, and the before/after state.
  • Versioned and documented: an OpenAPI specification is planned and will be published with the docs.

The current, read-only implementation is the admin-api module.


Rule 7: Log everything worthwhile​

Every worthwhile thing that happens on the server becomes a structured event, stored in one place and delivered to the web and to Discord. An event has a time, category, severity, actor, target, module, a short message and details.

Three kinds of event​

KindWhat it's forExamples
AuditWho changed what. Kept longest, never edited.staff and Hermes actions, module toggles, role changes, bans and mutes, config changes, API token use
GameWorthwhile things players didjoins and leaves, trades, large transfers, rare drops, boss deaths, quest completions, level-ups
SystemThe server's own healthstartup and shutdown, errors, lag spikes, memory warnings, failed saves, module failures

Security events (anti-cheat flags, rate-limit hits, rejected tokens, blocked chat, suspected duplication) cut across all three and are always flagged.

What counts as worthwhile​

Log what someone might need later to answer "what happened?", "who did it?", "is something wrong?" or "is this player being harmed?".

  • Always: every audit event, anything that moves value between players, every moderation action and report, every security flag, every error.
  • With a threshold: money transfers above an amount, drops of Epic rarity or higher, lag spikes lasting more than a few seconds.
  • Never as individual events: every step, hit or ordinary chat line — those go into metrics instead.
  • Chat is kept briefly so a /report can attach recent conversation as evidence, never streamed to Discord, and deleted after a short retention period.

Where it can be read​

  1. Database (source of truth): every event is stored in ClickHouse (log_events).
  2. Web: the admin panel's log viewer, once it exists. Until then the admin API serves the same data and Hermes can read it.
  3. Discord: hidden staff channels fed by webhooks — #log-audit, #log-moderation, #log-security, #log-economy, #log-server, and #log-alerts for urgent events with a staff mention. The Discord bot will later add interactive alerts on top.

Rules for the logs themselves​

  • Fail safe. Logging never slows down or crashes the game. Events go into a bounded queue and are written on a background thread; if the queue is full, the event is dropped and counted. If Discord is down, events wait and retry; they are always in the database first.
  • No secrets and no private data. Passwords, tokens, IP addresses and emails are never logged anywhere; the Redactor strips them before storage. Discord gets player names and a short summary only.
  • Tamper-evident. Audit events are chained with a SHA-256 hash, so an edited or deleted row shows up in HashChain.verify.
  • Never silently lost. With no database, audit events spill to <data>/log/audit-spill.jsonl and are replayed when it comes back.
  • Retention: chat snapshots briefly; game and system events for months; audit events longer. Exact periods are decided in the Phase 4 safety and legal review, because the players are young.
  • Every module logs through the library (ctx.log()). No module prints its own format or posts to Discord directly.

The implementation is the log library plus the logging module.


Database: ClickHouse, local only​

Every module that needs a database uses ClickHouse. There is one database and it runs on the same machine as the server, so data never leaves the host. Setup is on Getting started › Local ClickHouse.

Access rules​

  • Only the data library (and the log library, for its event store) talks to ClickHouse. Modules never open their own connections.
  • Each module owns its tables and ships its schema migrations through the data library; applied migrations are recorded in schema_migrations.
  • Hermes, the Discord bot and the web panel never connect to ClickHouse. They read through the admin API.
  • Queries are always parameterised; client input is never spliced into SQL.

Designing for ClickHouse​

ClickHouse is built for fast appends and analytics, not row-by-row updates, and it has no general multi-row transactions. Data is designed around that:

  • Logs and events: append-only MergeTree tables, partitioned by month, with TTLs for retention.
  • Mutable records (player settings, module state, characters): ReplacingMergeTree with a version column. A change inserts a new row; reads take the latest. The server keeps hot player data in memory and writes asynchronously.
  • Money and items (planned, Tier 3): an append-only ledger. Every change is one inserted entry — transaction id, player, item or currency, signed amount, reason, and a hash chaining it to the previous entry. Balances are the sum of the ledger. The transaction function checks the change against the in-memory balance held by the single game-server process, then writes all entries of a transaction in one insert, which ClickHouse applies atomically. Inserts carry a deduplication token, so a retry can't apply a transaction twice. Nothing is ever updated or deleted; corrections are reversing entries.

:::warning Single writer The ledger design relies on one game server writing it. Before running several game servers at once, this has to be revisited (for example by routing all value changes through one ledger service). :::