Configuration#
VonixGuardian reads a single JSON file, generated on first boot at
config/vonixguardian/config.json. Edit it, then run /vg reload to apply
everything that can be applied in-process.
<server root>/
├── config/vonixguardian/
│ ├── config.json ← this file
│ └── worlds/ ← optional per-world overrides
├── blacklist.txt ← optional log-suppression rules
└── vonixguardian.db ← SQLite backend (default)
The file is lenient on read and strict on write. Whole-line // comments and
trailing # … comments are stripped before parsing, and trailing commas are
tolerated. Anything the mod itself writes is pretty-printed, comment-free, and
saved atomically through a .tmp file plus rename.
Validation is all-or-nothing
On load, every field is validated. If any one value is out of range the whole config is rejected and the server keeps the previous values:
GuardianConfig validation failed (2 problems):
- lookup.defaultPageSize: must be in [1,50] (got 0)
- theme: must be one of [aqua, blue, gold, green, purple, red, white] (got teal)
Default file#
This is what a fresh install writes, with every value at its shipped default.
{
"database": {
"type": "sqlite",
"file": "vonixguardian.db",
"jdbcUrl": null,
"user": null,
"password": null,
"migrationTarget": null,
"hikari": {
"maxPoolSize": 10,
"connectionTimeoutMs": 30000,
"maxLifetimeMs": 1800000,
"leakDetectionMs": 0
}
},
"queue": {
"maxSize": 50000,
"flushIntervalMs": 5000,
"batchSize": 1000
},
"logFile": {
"enabled": true,
"directory": "logs/vonixguardian",
"gzipRotated": true,
"retentionDays": 30,
"forceSyncOnFlush": false
},
"actions": {
"logBlocks": true,
"logContainers": true,
"logItems": true,
"logEntities": true,
"logExplosions": true,
"logChat": true,
"logCommands": true,
"logSessions": true,
"logSigns": true,
"logInteractions": true,
"logWorldEvents": true,
"worldBlacklist": [],
"blockBlacklist": ["minecraft:air"],
"sourceBlacklist": [],
"itemBlacklist": [],
"entityBlacklist": [],
"entityBlockChangeCoalesceWindowMs": 500,
"entityBlockChangeMaxTracked": 8192,
"entityChangeAllowlist": [],
"entityChangeLogAllEntities": false,
"logNaturalBreaks": true,
"logTreeGrowth": true,
"logMushroomGrowth": true,
"logVineGrowth": true,
"logSculkSpread": true,
"logPortals": true,
"logWaterFlow": false,
"logLavaFlow": false,
"logFireExtinguish": true,
"logCampfireStart": true,
"logHopperMetaFilter": false,
"logDuplicateSuppression": true,
"logCancelledChat": false,
"mixinHotEvents": true
},
"permissions": {
"useLuckPerms": true,
"defaultOpLevel": 3,
"perNodeOpLevels": {}
},
"lookup": {
"defaultPageSize": 7,
"maxRadius": 10000,
"maxResultRows": 100000,
"maxConcurrent": 4
},
"privacy": {
"hashIps": false,
"salt": "vonix-guardian-default-salt-CHANGE-ME"
},
"purge": {
"minAgeSecondsConsole": 86400,
"minAgeSecondsInGame": 2592000,
"autoPurgeSeconds": 0,
"autoPurgeTime": "03:30"
},
"storage": {
"persistNbt": true
},
"rollback": {
"explosionSupplementalReach": 16,
"mutationBatchSize": 1000,
"requireConfirm": false
},
"theme": "aqua",
"language": "en_us"
}
Missing keys are filled from these defaults on boot, so an older config file keeps working after an upgrade.
database#
Selects the storage backend.
| Key | Type | Default | Valid | |
|---|---|---|---|---|
type |
string | sqlite |
sqlite, mysql, postgresql |
Backend selector. |
file |
string | vonixguardian.db |
non-blank when type is sqlite |
SQLite file, relative to the server root. |
jdbcUrl |
string | null |
non-blank for MySQL/PostgreSQL | Full JDBC URL. |
user |
string | null |
— | Database user. |
password |
string | null |
— | Database password. |
migrationTarget |
object | null |
see below | Destination descriptor for /vg migrate-db. |
hikari |
object | see below | see below | Connection-pool tuning. |
SQLite needs only file. The JDBC URL is derived as
jdbc:sqlite:<file>; providing an explicit jdbcUrl takes precedence. The path
is resolved against the server's working directory.
MySQL / MariaDB and PostgreSQL need jdbcUrl, user, and password:
"database": {
"type": "mysql",
"jdbcUrl": "jdbc:mysql://127.0.0.1:3306/vonixguardian",
"user": "guardian",
"password": "CHANGE-ME",
"hikari": { "maxPoolSize": 20 }
}
Validate type before restarting: a typo is rejected at load, and a bad
jdbcUrl surfaces as an UNHEALTHY storage state in /vg status.
Do not commit real credentials
config.json holds the database password in plain text when a remote backend
is used. Restrict file permissions on the server and keep the file out of
version control. VonixGuardian never logs the password value.
database.hikari#
Pool tuning. Defaults match HikariCP's own behaviour except for the pool size.
| Key | Type | Default | Valid | Meaning |
|---|---|---|---|---|
maxPoolSize |
int | 10 |
1–256 | Maximum pooled connections. |
connectionTimeoutMs |
long | 30000 |
≥ 250 | How long a borrower waits before failing. |
maxLifetimeMs |
long | 1800000 |
0 or ≥ 30000 |
Connection recycle age. 0 disables. |
leakDetectionMs |
long | 0 |
0 or ≥ 2000 |
Leak-warning threshold. 0 disables. |
Raise maxPoolSize for a busy server on MySQL or PostgreSQL. Leave it at 10 for
SQLite — SQLite serialises writes regardless of pool size, so a larger pool adds
contention without throughput.
database.migrationTarget#
Used only by /vg migrate-db. It describes
the destination backend and is never hot-swapped — the running server stays on its
active backend until an operator runs the migration.
"migrationTarget": {
"type": "postgresql",
"file": null,
"jdbcUrl": "jdbc:postgresql://127.0.0.1:5432/vonixguardian",
"user": "guardian",
"password": "CHANGE-ME"
}
| Key | Required for |
|---|---|
type |
Always. Must match the command's target argument. |
file |
type: sqlite. |
jdbcUrl |
type: mysql / postgresql. |
user |
type: mysql / postgresql. |
password |
type: mysql / postgresql. Must be non-null (may be empty). |
queue#
The async writer queue. Events are admitted on the producing thread and written in batches by a background writer.
| Key | Type | Default | Valid | Meaning |
|---|---|---|---|---|
maxSize |
int | 50000 |
> 0 | Admission cap before events are dropped with a WARN. |
flushIntervalMs |
long | 5000 |
> 0 | Maximum age of a queued event before it is forced out. |
batchSize |
int | 1000 |
> 0, ≤ maxSize |
Rows per INSERT batch. |
When the queue is full, new events are dropped and counted. /vg status reports
dropped, and the durable quarantine outbox retains what it can for replay.
Sizing. A batch of 1,000 rows is a good default for SQLite and fine for MySQL.
Raising batchSize improves throughput on a remote database but widens the window
of data at risk in a crash. Raising maxSize absorbs longer writer stalls at the
cost of memory — 50,000 events is roughly a few megabytes.
queue changes require a restart
maxSize, flushIntervalMs, and batchSize are restart-only. /vg reload
detects the change and reports queue under Requires restart, but the
running server keeps its original values.
logFile#
An independent JSON-Lines audit trail written alongside the database. Useful as a tamper-evident side channel and for ingesting into an external log pipeline.
| Key | Type | Default | Valid | Meaning |
|---|---|---|---|---|
enabled |
bool | true |
— | Write the rolling file. Hot-swappable. |
directory |
string | logs/vonixguardian |
non-blank when enabled | Directory for daily files. |
gzipRotated |
bool | true |
— | Gzip a rotated day's file. |
retentionDays |
int | 30 |
≥ 0 (0 = keep forever) |
How long rotated files are kept. |
forceSyncOnFlush |
bool | false |
— | FileChannel.force(false) on every flush. |
Files are named audit-YYYY-MM-DD.log.jsonl. Rotation is lazy — it happens on the
next write after the UTC date changes, not at midnight.
forceSyncOnFlush trades durability for latency. With it false, a JVM crash can
lose up to queue.flushIntervalMs of the most recent log lines. Set it true if
the log file is your audit system of record and the host's storage is local. Leave
it false on network or slow storage, where a per-batch fsync stalls the writer.
directory, gzipRotated, and retentionDays are restart-only. enabled and
forceSyncOnFlush are hot-swappable.
actions#
What gets recorded. Two layers: coarse category toggles, then per-event toggles and blacklists.
Category toggles#
| Key | Default | Covers |
|---|---|---|
logBlocks |
true |
Block place and break. |
logContainers |
true |
Container deposit and withdrawal. |
logItems |
true |
Item drop, pickup, craft. |
logEntities |
true |
Entity kill and spawn. |
logExplosions |
true |
Explosions. |
logChat |
true |
Chat messages. |
logCommands |
true |
Commands. |
logSessions |
true |
Join, leave, username change. |
logSigns |
true |
Sign edits. |
logInteractions |
true |
Block interactions (buttons, levers, doors). |
logWorldEvents |
true |
Non-player world events. |
Per-event toggles#
Finer control, matching CoreProtect's per-listener configurability.
| Key | Default | Records |
|---|---|---|
logNaturalBreaks |
true |
Crops trampled, tall grass broken by a mob. |
logTreeGrowth |
true |
Sapling to tree. |
logMushroomGrowth |
true |
Huge-mushroom growth. |
logVineGrowth |
true |
Vine, kelp, cave-vine growth. |
logSculkSpread |
true |
Sculk-catalyst spread. |
logPortals |
true |
Nether and end portal frame formation. |
logWaterFlow |
false |
Water-flow block changes. Expensive. |
logLavaFlow |
false |
Lava-flow block changes. Expensive. |
logFireExtinguish |
true |
Fire extinguished by rain, water, or a player. |
logCampfireStart |
true |
Campfire lighting. |
logHopperMetaFilter |
false |
Skip hopper repeats with identical NBT. |
logDuplicateSuppression |
true |
Collapse identical consecutive rows. |
logCancelledChat |
false |
Record chat even when another plugin cancels it. |
mixinHotEvents |
true |
Master switch for hot-tick mixin events. |
logWaterFlow and logLavaFlow ship off because fluid updates are per-tick
block changes; on a large farm or an ocean build they can dominate the row count.
Enable them when fluid griefing is a real problem on your server, and pair them
with a purge horizon.
mixinHotEvents is a kill switch, not a feature toggle. When false, the engine
short-circuits any event whose source tag begins with #fire, #natural, or
#dispenser before it reaches the gate or the queue. That is the emergency lever
when a mixin pipeline is drowning the writer during a lag spike. Leave it true.
Blacklists#
Values are lists of strings. A null list is tolerated and treated as empty.
| Key | Default | Skips |
|---|---|---|
worldBlacklist |
[] |
Whole worlds, e.g. minecraft:the_end. |
blockBlacklist |
["minecraft:air"] |
Block ids. |
sourceBlacklist |
[] |
Source tags, e.g. #tnt. |
itemBlacklist |
[] |
Item ids. |
entityBlacklist |
[] |
Entity ids. |
"blockBlacklist": ["minecraft:air", "minecraft:stone", "minecraft:dirt"],
"entityBlacklist": ["minecraft:bat", "minecraft:armor_stand"],
"worldBlacklist": ["minecraft:the_end"]
Blacklisting a common block such as stone dramatically reduces row volume on a mining-heavy server, at the cost of losing the ability to roll back that block.
These are config lists, not blacklist.txt
actions.*Blacklist are JSON arrays edited in config.json. The separate
blacklist.txt file at the server root uses a different line-based grammar and
supports per-user and per-command rules. Both are applied; see
Blacklist file.
Modded entity griefing#
This is the section that matters on a modded pack.
| Key | Type | Default | Valid | Meaning |
|---|---|---|---|---|
entityChangeAllowlist |
list | [] |
see below | Extra entities to record. |
entityChangeLogAllEntities |
bool | false |
— | Record every entity block change. |
entityBlockChangeCoalesceWindowMs |
long | 500 |
≥ 0 (negative disables) | Dedup window per (actor, coord). |
entityBlockChangeMaxTracked |
int | 8192 |
> 0 | Coalescer LRU capacity. |
entityChangeAllowlist entries accept three shapes:
| Shape | Example | Matches |
|---|---|---|
| Exact key | isleofberk:night_fury |
That entity. |
| Namespace wildcard | isleofberk:* |
Every entity in the namespace. |
| Bare namespace | isleofberk |
Same as isleofberk:*. |
An empty allowlist means vanilla-only: the hardcoded CoreProtect-parity set of enderman, ender dragon, wither, ravager, silverfish, turtle, fox, zombie, and falling block.
Manage the allowlist at runtime with
/vg entitylog add|remove|list rather than by
hand — the command writes the file and reloads it under a lock.
entityChangeLogAllEntities is a diagnostic flag
Setting it true bypasses the allowlist and records every
LivingDestroyBlockEvent. On Forge and NeoForge that event is a prospective
permission query fired per mob tick, so a single flying dragon can fire it
hundreds of thousands of times per second without destroying anything. On a
modded pack this will flood the queue, inflate the database, and drop events.
Use the allowlist instead.
The coalescer is what makes modded packs viable. It collapses repeated
(actor, coordinate) hits inside the window into one audit row. Setting
entityBlockChangeCoalesceWindowMs to 0 is rewritten to 500 on load — a
deliberate guard against accidentally disabling it. To genuinely disable
coalescing you must set a negative value.
permissions#
| Key | Type | Default | Valid | Meaning |
|---|---|---|---|---|
useLuckPerms |
bool | true |
— | Try the LuckPerms bridge first. |
defaultOpLevel |
int | 3 |
0–4 | Coarse op fallback when LP is absent. |
perNodeOpLevels |
object | {} |
values 0–4 | Per-node op-level override map. |
useLuckPerms: true is safe when LuckPerms is not installed — detection is by
reflection and a missing LP simply falls through to op level.
perNodeOpLevels lets you tune the fallback per node, which is otherwise
impossible without LuckPerms:
"perNodeOpLevels": {
"vonixguardian.lookup": 2,
"vonixguardian.rollback": 3,
"vonixguardian.purge": 4
}
Unknown keys are logged as a WARN and ignored; values outside 0–4 are a hard
validation error. permissions is restart-only.
See Permissions for the complete node list and resolution order.
lookup#
Query UX and backpressure.
| Key | Type | Default | Valid | Meaning |
|---|---|---|---|---|
defaultPageSize |
int | 7 |
1–50 | Rows per page when unspecified. |
maxRadius |
int | 10000 |
≥ 1 | Largest accepted numeric r:. |
maxResultRows |
int | 100000 |
100–10000000 | Absolute row cap per query. |
maxConcurrent |
int | 4 |
1–64 | Concurrent query permits. |
defaultPageSize is also clamped to 50 at the command layer, so a config value of
50 is the practical maximum. maxResultRows is the hard ceiling the DAO refuses to
exceed; a query that would materialise more rows fails rather than exhausting heap.
maxConcurrent sizes a semaphore around the read path. It is restart-only. Raise it
on a server with many staff running lookups; lower it if queries compete with ticks.
privacy#
| Key | Type | Default | Valid | Meaning |
|---|---|---|---|---|
hashIps |
bool | false |
— | Hash IP and hostname values before storing. |
salt |
string | vonix-guardian-default-salt-CHANGE-ME |
≥ 16 chars when hashing | Salt prefix. |
Default behaviour stores the raw connecting address on session rows. If your
privacy policy does not allow that, set hashIps: true and change the salt.
Hashing fails closed on the placeholder salt
hashIps: true with the shipped placeholder salt is a hard validation error
— the server refuses the config rather than silently producing trivially
reversible hashes:
privacy.salt: must be changed from the default placeholder when
privacy.hashIps is true; set a random >=16 char salt or set hashIps=false
Generate a salt once, store it with your other secrets, and never change it afterwards — a changed salt makes every stored hash unlinkable to new rows.
privacy.salt is restart-only. privacy.hashIps is hot-swappable.
purge#
Retention floors and the optional background purge daemon.
| Key | Type | Default | Valid | Meaning |
|---|---|---|---|---|
minAgeSecondsConsole |
long | 86400 |
≥ 60 | Console purge floor (24 h). |
minAgeSecondsInGame |
long | 2592000 |
≥ 3600 | In-game purge floor (30 d). |
autoPurgeSeconds |
long | 0 |
0 or ≥ 2592000 |
Retention horizon. 0 = daemon off. |
autoPurgeTime |
string | 03:30 |
HH:mm, 24 h |
Daily run time, server local. |
autoPurgeSeconds ships as 0, which means unbounded retention: the daemon
does not run and rows accumulate until an operator purges manually or sets a
horizon. 7776000 (90 days) is the suggested horizon for a busy server and is
not applied for you.
"purge": {
"autoPurgeSeconds": 7776000,
"autoPurgeTime": "04:00"
}
With a non-zero value the daemon runs daily at autoPurgeTime, deleting rows older
than the horizon. /vg status reports either the enabled state and rows purged this
restart, or 0 = unbounded with the suggested value.
The floor is not the horizon
minAgeSecondsInGame limits how recent a purge may target. autoPurgeSeconds
is how much history to keep. A server with a 30-day in-game floor and a
90-day horizon keeps 90 days; the floor only prevents /vg purge t:2d.
purge is fully hot-swappable, including the daemon schedule.
storage#
| Key | Type | Default | Valid | Meaning |
|---|---|---|---|---|
persistNbt |
bool | true |
— | Capture block entity, item, and entity NBT. |
With persistNbt: true, producers capture full block-state properties and raw NBT
bytes so a rollback restores chest contents, sign text, and entity data faithfully.
That is the safe default and the reason a rolled-back chest comes back full.
With persistNbt: false, those columns stay empty. Inventory rollback and restore
then refuse rows that lack a full payload rather than restoring a lossy
approximation. Set it false only if storage cost outweighs fidelity for your
server, and understand that container rollback becomes unreliable.
storage.persistNbt is hot-swappable.
rollback#
| Key | Type | Default | Valid | Meaning |
|---|---|---|---|---|
explosionSupplementalReach |
int | 16 |
0–1024 | Padding for the supplemental explosion scan. |
mutationBatchSize |
int | 1000 |
≥ 1 | World mutations applied per tick slice. |
requireConfirm |
bool | false |
— | Stop after preview and wait for /vg apply. |
explosionSupplementalReach solves a real problem: explosion rows record the blast
centre, so a blast whose centre sits outside your radius but whose damage
reaches inside it would be missed. The supplemental scan keeps the spatial
predicate but pads it outward by this many blocks. 16 mirrors vanilla TNT reach;
raise it for modded explosives with a larger radius. Widening it only relaxes a
pre-filter — row admission still verifies the affected list, so it never
over-admits.
mutationBatchSize is the per-tick slice of world edits during a rollback. Lower it
if a large rollback causes visible tick spikes; raise it for faster completion on a
capable server.
requireConfirm off (the default) means /vg rollback and /vg restore apply
immediately at op level 3. Set it true on a production server where an accidental
global rollback is the bigger risk:
/vg rollback u:Griefer t:24h r:#global ← now stops after previewing
/vg apply ← explicit confirmation
Existing configs keep applying immediately until an operator sets the flag. There is no automatic replay of an open batch on boot.
theme and language#
| Key | Default | Valid |
|---|---|---|
theme |
aqua |
aqua, blue, gold, green, purple, red, white |
language |
en_us |
see below |
Themes colour command output. language selects a message bundle from the 15
shipped bundles: en_us, en, de, es, fr, ja, ko, pl, ru, tr,
tt, uk, vi, zh_cn, zh_tw. Both are hot-swappable.
Reload vs restart#
/vg reload re-reads the file and applies what is safe. Anything it cannot apply
is reported by name.
| Subsection | /vg reload |
|---|---|
actions.* (all toggles, blacklists, coalescer, allowlist) |
Hot |
logFile.enabled, logFile.forceSyncOnFlush |
Hot |
lookup.defaultPageSize, lookup.maxRadius, lookup.maxResultRows |
Hot |
privacy.hashIps |
Hot |
purge.* (including the daemon schedule) |
Hot |
storage.persistNbt |
Hot |
rollback.* (reach, batch size, requireConfirm) |
Hot |
theme, language |
Hot |
blacklist.txt, config/vonixguardian/worlds/ |
Hot |
database.* |
Restart |
queue.* |
Restart |
logFile.directory, gzipRotated, retentionDays |
Restart |
permissions.* |
Restart |
lookup.maxConcurrent |
Restart |
privacy.salt |
Restart |
A restart-only change is detected and named but not applied — the live server keeps the old value, and the reload output says so:
[VonixGuardian] Hot-swapped: 1 actions
[VonixGuardian] Requires restart: 1 database
[VonixGuardian] Errors: 0 (none)
Per-world overrides#
Any subset of the actions block can be overridden per world, mirroring
CoreProtect's world_nether.yml pattern.
Create config/vonixguardian/worlds/<world_key>.json, where the world key is the
registry id with : replaced by __:
config/vonixguardian/worlds/
├── minecraft__overworld.json
├── minecraft__the_nether.json
└── minecraft__the_end.json
The file is a partial actions object. Fields you omit inherit from the root
actions block.
{
"logWaterFlow": false,
"logLavaFlow": false,
"logBlocks": true,
"entityChangeAllowlist": ["twilightforest:*"]
}
That example keeps full block logging in the Nether but enables the modded entity allowlist only there.
To disable all logging in the End, set every toggle to false in
minecraft__the_end.json, or simply add minecraft:the_end to the root
worldBlacklist.
Overrides reload with /vg reload, are reported in /vg status under
Per-world overrides, and are cached by file modification time so an unchanged
file is not re-parsed.
Blacklist file#
blacklist.txt at the server root (not in the config directory) suppresses
specific logging with a line-based grammar. One rule per line; blank lines and
lines starting with # are ignored.
| Rule | Example | Suppresses |
|---|---|---|
user:<name> |
user:Notch |
That player's actions. |
user_uuid:<uuid> |
user_uuid:069a79f4-44e9-4726-a5be-fca90e38aaf5 |
Same, by UUID. |
command:<name> |
command:help |
That command (lower-cased prefix). |
block:<id> |
block:minecraft:stone |
That block id. |
entity:<id> |
entity:minecraft:creeper |
That entity id. |
<id>@<user> |
minecraft:shears@#dispenser |
That id, only for that actor. |
# Suppress a staff account entirely
user:BuildBot
# Don't record the help command
command:help
# Ignore stone and dirt — too much volume
block:minecraft:stone
block:minecraft:dirt
# Never log shears dispensed by a dispenser
minecraft:shears@#dispenser
Unknown rule kinds are logged as a WARN and skipped; a malformed file never aborts
boot. /vg status reports the loaded rule count, and /vg reload re-parses it and
reports the count under blacklist.txt.
Applying changes without editing the file#
For the hot-swappable subset you can skip the editor entirely:
/vg config get lookup.maxRadius
/vg config set lookup.maxRadius 20000
/vg config set actions.entityBlacklist minecraft:zombie,create:contraption
/vg config set actions.itemBlacklist none
See Commands → config for the full list of settable keys. Everything else is file-only by design.