Vonix Server Utilities Vonix Mods v2.2.0 Search GitHub
Vonix Server Utilities/Operations/Database & storage

Database and storage#

VSU stores everything persistent in one SQLite database. There is no external database option, no MySQL or PostgreSQL path, and no second store.

config/vonix_server_utilities/data.db

The path is fixed relative to the server directory and is not configurable. On first start the directory is created if needed and the schema is applied.

Connection model#

VSU uses one persistent JDBC connection for the process lifetime.

  • The connection is opened in Database.init during the server-starting phase.
  • All reads and writes are dispatched to a single-threaded executor named VonixSU-DB. Every repository method documents and assumes this.
  • Results are bounced back to the main tick thread with server.execute(...) before any Minecraft object is touched.

The single-thread design is what makes a single SQLite connection safe: writes serialise by construction, so there is no lock contention between VSU's own tasks. Player-facing commands queue a task and return immediately, which is why /sethome and /home do not stall the tick.

Two pragmas are applied at open time:

PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;

WAL lets readers proceed while a write is in flight and is what allows the moderation listeners to query punishments from a network thread without blocking the tick. synchronous=NORMAL trades a small durability window on an OS crash for noticeably faster commits; the database stays consistent, but a hard power loss can lose the last few transactions. That is the normal trade for a game server.

The JDBC driver is loaded explicitly with Class.forName rather than relying on service discovery, because Forge and NeoForge mod class loaders are invisible to DriverManager's bootstrap loader. The driver class is relocated into the mod's own namespace during the build, so a second mod shipping a different SQLite version cannot conflict with it.

While the server runs you will see three files:

data.db
data.db-wal
data.db-shm

The -wal and -shm files are part of WAL mode. They are not corruption and must not be deleted while the server is running.

Tables#

Thirteen tables exist in 2.2.0.

Table Contents
vsu_homes Player homes, one row per player per name
vsu_warps Server warps, one row per name
vsu_kit_cooldowns Kit claim timestamps, keyed by player, kit, and group
vsu_back_locations Last teleport origin and last death position per player
vsu_nicknames Display nickname per player
vsu_ignore_list Directed ignore pairs
vsu_migration One-time migration markers
punishments Bans, mutes, kicks, and warnings with audit fields
vsu_crates Crate definitions
vsu_crate_prizes Weighted prizes per crate
vsu_player_keys Virtual key balances per player per type
vsu_crate_claims One row per crate open, with status
vsu_playtime_key_progress Playtime intervals already awarded

vsu_homes#

CREATE TABLE vsu_homes (
    uuid  TEXT NOT NULL,
    name  TEXT NOT NULL,
    world TEXT NOT NULL,
    x     REAL NOT NULL,
    y     REAL NOT NULL,
    z     REAL NOT NULL,
    yaw   REAL NOT NULL,
    pitch REAL NOT NULL,
    PRIMARY KEY (uuid, name)
);
CREATE INDEX idx_vsu_homes_uuid ON vsu_homes (uuid);

Home names are stored lower-cased, so Base and base are the same home. The world column holds the dimension id string, for example minecraft:overworld. A home in a dimension that is no longer loaded reports "Home world is unavailable" rather than failing silently.

vsu_warps#

CREATE TABLE vsu_warps (
    name       TEXT PRIMARY KEY,
    world      TEXT NOT NULL,
    x          REAL NOT NULL,
    y          REAL NOT NULL,
    z          REAL NOT NULL,
    yaw        REAL NOT NULL,
    pitch      REAL NOT NULL,
    created_by TEXT,
    created_at INTEGER NOT NULL DEFAULT 0
);

Warp names are also lower-cased and are the primary key, so /setwarp Shop overwrites an existing shop. created_by is a UUID string and created_at is epoch seconds, not milliseconds — the only table in the schema that uses seconds.

vsu_kit_cooldowns#

CREATE TABLE vsu_kit_cooldowns (
    uuid        TEXT NOT NULL,
    kit_name    TEXT NOT NULL,
    claim_group TEXT NOT NULL,
    last_used   INTEGER NOT NULL,
    PRIMARY KEY (uuid, kit_name)
);
CREATE INDEX idx_vsu_kit_cooldowns_group ON vsu_kit_cooldowns (uuid, claim_group);

last_used is epoch seconds. Eligibility is decided by the maximum last_used across the player's rows in the same claim_group. The group column is added and backfilled in place on startup if an older database lacks it; the migration is idempotent and never drops rows.

vsu_back_locations#

CREATE TABLE vsu_back_locations (
    uuid       TEXT NOT NULL,
    world      TEXT NOT NULL,
    x          REAL NOT NULL,
    y          REAL NOT NULL,
    z          REAL NOT NULL,
    yaw        REAL NOT NULL,
    pitch      REAL NOT NULL,
    kind       TEXT NOT NULL,
    updated_at INTEGER NOT NULL,
    PRIMARY KEY (uuid, kind)
);

kind is either tp or death, which is what keeps /back and /backdeath independent. updated_at is epoch milliseconds. Rows are written through from the in-memory cache, so a restart does not lose them.

vsu_nicknames#

CREATE TABLE vsu_nicknames (
    uuid       TEXT PRIMARY KEY,
    nickname   TEXT NOT NULL,
    updated_at INTEGER NOT NULL
);

The nickname is stored with section-sign colour codes already translated from the & form the player typed.

vsu_ignore_list#

CREATE TABLE vsu_ignore_list (
    owner_uuid  TEXT NOT NULL,
    target_uuid TEXT NOT NULL,
    created_at  INTEGER NOT NULL,
    PRIMARY KEY (owner_uuid, target_uuid)
);

Directed pairs: A ignores B does not imply B ignores A.

vsu_migration#

CREATE TABLE vsu_migration (
    key   TEXT PRIMARY KEY,
    value TEXT NOT NULL
);

A generic key/value marker table. In 2.2.0 it holds exactly one row in normal operation, described under migration below.

punishments#

CREATE TABLE punishments (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    type        TEXT NOT NULL,
    target_uuid TEXT NOT NULL,
    target_name TEXT NOT NULL,
    issuer_uuid TEXT,
    issuer_name TEXT NOT NULL,
    reason      TEXT,
    issued_at   INTEGER NOT NULL,
    expires_at  INTEGER,
    active      INTEGER NOT NULL DEFAULT 1,
    revoked_by  TEXT,
    revoked_at  INTEGER
);
CREATE INDEX idx_punishments_target_active ON punishments(target_uuid, active, type);
CREATE INDEX idx_punishments_expires ON punishments(active, expires_at);

All timestamps are epoch milliseconds. expires_at null means permanent. issuer_uuid null means console. Full query examples are on Moderation.

Crate tables#

CREATE TABLE vsu_crates (
    name TEXT PRIMARY KEY, key_type TEXT NOT NULL,
    enabled INTEGER NOT NULL DEFAULT 1, updated_at INTEGER NOT NULL);

CREATE TABLE vsu_crate_prizes (
    crate_name TEXT NOT NULL, prize_id INTEGER NOT NULL,
    label TEXT NOT NULL, command TEXT NOT NULL, weight INTEGER NOT NULL,
    PRIMARY KEY (crate_name, prize_id),
    FOREIGN KEY (crate_name) REFERENCES vsu_crates(name) ON DELETE CASCADE);

CREATE TABLE vsu_player_keys (
    uuid TEXT NOT NULL, key_type TEXT NOT NULL,
    amount INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL,
    PRIMARY KEY (uuid, key_type));

CREATE TABLE vsu_crate_claims (
    claim_id TEXT PRIMARY KEY, uuid TEXT NOT NULL, crate_name TEXT NOT NULL,
    key_type TEXT NOT NULL, prize_id INTEGER NOT NULL, label TEXT NOT NULL,
    command TEXT NOT NULL, status TEXT NOT NULL, created_at INTEGER NOT NULL,
    completed_at INTEGER);

CREATE TABLE vsu_playtime_key_progress (
    uuid TEXT PRIMARY KEY, awarded_intervals INTEGER NOT NULL DEFAULT 0,
    updated_at INTEGER NOT NULL);

vsu_crate_claims.status is one of PENDING, COMPLETED, or REFUNDED. Foreign keys are enabled for these tables, and deleting a crate cascades to its prizes; the repository also deletes prizes explicitly inside the same transaction.

Home, warp, and punishment flow#

Reads are cache-first or database-only depending on the feature:

Feature Read path Write path
Homes Database on each command Database, then main-thread message
Warps Database on each command Database
Nicknames In-memory map, hydrated at start Memory plus async database write
Ignore list In-memory map, hydrated at start Memory plus async database write
Back locations In-memory map, hydrated at start Memory plus async database write
Mute state In-memory set, hydrated at start Memory immediately, database async
Punishments Database Database
Keys and crates Database Database, transactional

Nicknames, ignore lists, mutes, and back locations hydrate during the server-starting phase on the database executor, so a restart restores them before players can act.

VonixCore migration#

The first time VSU starts against a server that previously ran VonixCore, it imports homes, warps, and kit cooldowns. The import is one-time and guarded by a marker row:

SELECT value FROM vsu_migration WHERE key='vonixcore_migrated'

Behaviour:

  1. If the marker is true, the migration is skipped with a log line saying so.
  2. Otherwise VSU looks for a VonixCore database at <world>/vonixcore/vonixcore.db, then <server>/vonixcore/vonixcore.db, then any *.db in <world>/vonixcore/.
  3. If nothing is found, the marker is written and the migration is skipped, so the lookup never runs again.
  4. If a source database is found, it is copied to vonixcore.db.bak-<epoch-millis> next to the original before anything is read.
  5. The source is opened read-only and homes, warps, and kit cooldowns are imported in a single transaction.

Source table names are probed, not assumed: vc_homes then vonixcore_homes; vc_warps then vonixcore_warps; vc_kit_cooldowns, vonixcore_kit_cooldowns, then kit_cooldowns. Rows are inserted with INSERT OR IGNORE, so a re-run cannot duplicate a home or warp. Warps preserve created_by and created_at when the source has those columns; when it does not, created_by is left null and created_at is set to the import time. Imported kit cooldowns get their claim_group defaulted from the kit name.

If any of the three table imports throws, the whole transaction rolls back and the marker is not written. The log records which table failed and states that the migration will be re-attempted. That is deliberate: a partial import is worse than a repeated one. Fix the source database and restart.

To re-run the migration on purpose, delete the marker:

DELETE FROM vsu_migration WHERE key='vonixcore_migrated';

The next start will look for the source database again. Because inserts ignore duplicates, re-running against the same source is harmless; it will simply import nothing new.

Backup#

Back up the whole config/vonix_server_utilities/ directory, not just data.db.

Clean method — stop the server first. With the server stopped, data.db is complete and the -wal sidecars are checkpointed. Copy the directory:

systemctl stop minecraft
tar czf vsu-backup-$(date +%F).tar.gz -C config vonix_server_utilities
systemctl start minecraft

Live method — use the SQLite CLI. Copying data.db while the server runs can capture a database without its WAL content. Ask SQLite for a consistent snapshot instead:

sqlite3 config/vonix_server_utilities/data.db \
  ".backup '/backups/vsu-$(date +%F-%H%M).db'"

.backup produces a single consistent file with no sidecars, which is what you want for an archive or for restoring onto another host. A plain cp of a live database is the most common way to end up with a backup that is missing the last few minutes of activity or, worse, is unopenable.

Restore. Stop the server, replace data.db with the backup, and delete any stray data.db-wal and data.db-shm files left over from the old database. A WAL file from a different database will be rejected or, worse, misapplied; removing it is the correct step.

Direct SQL#

Editing the database directly is supported for bulk operations and audit, with two rules:

  • Stop the server, or use the sqlite3 CLI on a copy. The mod holds a persistent connection and will not see your changes until restart.
  • Prefer active=0 over DELETE for punishments. History is the point of the table.

/vonixsu reload does not re-read the database. After direct SQL, restart so the in-memory caches for mutes, nicknames, ignore lists, and back locations are rebuilt.