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.initduring 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:
- If the marker is
true, the migration is skipped with a log line saying so. - Otherwise VSU looks for a VonixCore database at
<world>/vonixcore/vonixcore.db, then<server>/vonixcore/vonixcore.db, then any*.dbin<world>/vonixcore/. - If nothing is found, the marker is written and the migration is skipped, so the lookup never runs again.
- If a source database is found, it is copied to
vonixcore.db.bak-<epoch-millis>next to the original before anything is read. - 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
sqlite3CLI on a copy. The mod holds a persistent connection and will not see your changes until restart. - Prefer
active=0overDELETEfor 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.