Configuration#
Viscord reads one TOML file:
config/viscord/viscord.toml
The file is generated on first boot with every key present and commented. Values
below are taken from config/toml/ViscordConfigToml.java (the typed accessors
that read the values) and config/toml/TomlConfigManager.java (the spec that
writes and repairs the file). Where the two disagree, the accessor is what the
running server uses.
There are 46 keys declared across thirteen tables. Two of them (discord.webhook_id
and discord.events.webhook_url) are written by the generator but read by no
code path — they are marked spec-only below. The other 44 drive behaviour.
Every key is optional in the sense that a missing value falls back to the
compiled default, but the three placeholder secrets must be replaced before
anything connects.
Reload
/viscord reload closes the platform connection, re-reads the file, and
reconnects. No server restart is needed for any key on this page. See
Upgrades and reload.
[general]#
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | false |
Master switch. False means Viscord initialises nothing at startup and logs Disabled in config. |
platform |
string | "discord" |
Which platform to connect: discord, fluxer, or both. |
debug |
bool | false |
Verbose logging. Also enables the rate-limiter's info line naming the bucket that limited a call. |
platform = "both" connects Discord and Fluxer independently: chat and events
are duplicated to both, but a message on one platform is not relayed to the
other. Cross-platform relay is what [general.tridirectional] adds.
[general.tridirectional]#
Three-way relay between Discord, Fluxer, and Minecraft. Requires Discord and
Fluxer to both be configured; enabling this switches the connection path even if
platform is still discord.
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | false |
Master switch for tridirectional relay. |
discord_to_fluxer |
bool | true |
Relay Discord messages into Fluxer. |
fluxer_to_discord |
bool | true |
Relay Fluxer messages into Discord. |
show_source |
bool | true |
Prefix bridged messages with [Discord] or [Fluxer]. |
With show_source = false a bridged line is author: message with no platform
tag. Turn it off only when the two channels are visually distinct anyway;
the tag is also what the echo guard uses to spot Discord-originated text
arriving back from Fluxer.
[discord]#
| Key | Type | Default | Meaning |
|---|---|---|---|
bot_token |
string | "YOUR_BOT_TOKEN_HERE" |
Bot token from the Discord developer portal. |
channel_id |
string | "YOUR_CHANNEL_ID_HERE" |
Channel Viscord watches and posts chat to. Comma-separated values watch several channels. |
webhook_url |
string | "" |
Webhook for Minecraft → Discord chat. Empty means outbound chat is dropped. |
webhook_id |
string | "" |
Spec-only. Written to the file; no accessor reads it. |
invite_url |
string | "" |
Invite link shown by /discord and used as the click target on the [Discord] tag in chat. |
channel_id is also the source of the guild for native command registration:
Viscord resolves each configured channel, finds its guild, and synchronises
/link, /list, /home, /help, and /status there. If no configured channel
resolves, no native commands are registered at all.
The webhook ID used by the echo guard is extracted from webhook_url at runtime
by splitting on / and taking the numeric segment after webhooks. There is no
separate key to set it.
Correction to older docs
Earlier Viscord documentation listed discord.webhook_id as a settable key
that you only need to change for an explicit override. No accessor for it
exists in ViscordConfigToml.java: the webhook ID used by the echo guard is
always parsed out of discord.webhook_url at runtime. The generator still
writes a webhook_id = "" line because the config spec defines it, so the key
is present in your file — but editing it changes nothing. Do not rely on it.
[discord.events]#
| Key | Type | Default | Meaning |
|---|---|---|---|
channel_id |
string | "" |
Separate channel for join/leave/death/advancement embeds. Empty means events go to discord.channel_id. |
webhook_url |
string | "" |
Spec-only. Written to the file; event embeds always go through the bot API. |
Event embeds are sent by the bot, not the webhook, so this channel must be one
the bot can see and post in. When set, Viscord also reads this channel, which
is how one server's event embeds become another server's in-game notifications.
Because delivery is bot-API only, a separate event webhook cannot be configured —
the webhook_url line here is written by the generator but has no effect.
[fluxer]#
| Key | Type | Default | Meaning |
|---|---|---|---|
bot_token |
string | "YOUR_FLUXER_BOT_TOKEN" |
Bot token from the Fluxer developer portal. |
channel_id |
string | "YOUR_FLUXER_CHANNEL_ID" |
Fluxer channel for chat. Comma-separated values are all watched. |
event_channel_id |
string | "" |
Separate event channel. Empty means fluxer.channel_id. |
webhook_url |
string | "" |
Optional webhook for per-player names and avatars on Fluxer. Without it, messages post through the bot API as **name**: text. |
Fluxer's webhook URL format is
https://api.fluxer.app/v1/webhooks/{id}/{token}. Viscord parses the ID from
that shape and registers it as the bot's own webhook so only its echoes are
suppressed; other webhooks in the channel are treated as cross-server traffic.
fluxer.bot_token is validated against the literal placeholder string as well as
against empty, so leaving the default in place produces a clear
Bot token not configured! error rather than an authentication loop.
[server]#
| Key | Type | Default | Meaning |
|---|---|---|---|
prefix |
string | "[MC]" |
Server tag in bridged messages, and the anchor for the echo-loop guard. Must be unique per server in a shared channel. |
name |
string | "Minecraft Server" |
Display name used in embed authors, /list output, and bot status context. |
avatar_url |
string | https://minotar.net/armor/bust/{uuid}/100.png |
Template for player avatars on webhook messages. Placeholders: {uuid} (no dashes) and {username}. |
avatar_url is a template, not a fixed URL. Viscord substitutes the dashless
UUID when one is known and falls back to the username otherwise. If the value is
blank it uses the same Minotar URL as a built-in fallback, so blank does not mean
"no avatar". When SkinRestorer is installed and the player has a saved custom
skin, Viscord resolves https://mc-heads.net/avatar/<texture-hash>/100.png
first and ignores this template.
Correction to older docs
Earlier documentation gave server.avatar_url a default of "". The
compiled default in ViscordConfigToml.Server.AVATAR_URL is
https://minotar.net/armor/bust/{uuid}/100.png, and that is the value
written into a fresh viscord.toml.
[messages]#
| Key | Type | Default | Meaning |
|---|---|---|---|
discord_to_minecraft |
string | "[Discord] {username}: {message}" |
Format for platform → Minecraft chat. Placeholders: {username}, {message}. |
minecraft_to_discord |
string | "{message}" |
Minecraft → platform format. Accepted and persisted, but not applied by the 5.2.1 send path. |
webhook_username |
string | "{prefix} {username}" |
Webhook author name on Minecraft → Discord chat. Placeholders: {prefix}, {username}. |
use_display_name |
bool | true |
Resolve Discord authors as server nickname / global display name; false uses the plain account name. |
Two behaviours worth knowing before you customise these:
discord_to_minecraftis also used for Fluxer chat. Viscord replaces the literal[Discord]in the rendered result with[Fluxer], so a custom prefix like[Chat]stays as written and is not rewritten.- If the rendered
discord_to_minecraftstring contains[Discord]anddiscord.invite_urlis set, that tag becomes a clickable component that opens the invite. Everything around it stays plain text.
webhook_username matters beyond appearance: the echo guard prefixes a heuristic
on it. Viscord splits the format at {prefix} and checks whether an incoming
webhook author name starts with the text before the placeholder followed by
server.prefix. Moving {prefix} to the end of the format is legal but weakens
that particular check.
Correction to older docs
minecraft_to_discord is documented upstream as the Minecraft → Discord
chat format with a {message} placeholder. No code path reads this key in
5.2.1 — the outbound text is webhook_username plus the in-game message
after DiscordFormatter.convertToDiscordFormatting. The key is written by the
spec and survives migration, but editing it changes nothing. Use
webhook_username for the author line and [filters.chat] to control what
reaches Discord at all.
[messages.events]#
| Key | Type | Default | Meaning |
|---|---|---|---|
join |
bool | true |
Post an embed when a player joins. |
leave |
bool | true |
Post an embed when a player leaves. |
death |
bool | true |
Post the localised death message. |
advancement |
bool | true |
Post advancement notifications. |
Each flag is checked before the platform call, so disabling one removes the network work entirely rather than filtering it later. Advancements are additionally deduplicated: the same player and title within five seconds is dropped, because a single unlock can fire several events.
[filters]#
| Key | Type | Default | Meaning |
|---|---|---|---|
ignore_bots |
bool | true |
Drop messages whose author is a bot. |
ignore_webhooks |
bool | true |
Drop messages from other webhooks. |
trusted_bot_ids |
string | "" |
Comma-separated bot or webhook IDs that bypass both ignore filters. |
filter_by_prefix |
bool | true |
Drop bot and webhook authors whose display name starts with this server's prefix. |
show_other_server_events |
bool | true |
Accept event embeds posted by other Viscord servers. |
These five interact, and the interaction is the usual cause of "cross-server chat is silent". The exact rules are in Multi-server. The short version: prefix and self-origin checks apply only to bot and webhook authors, never to ordinary Discord users, and a message from this server's own bot or webhook is dropped unconditionally before any filter is consulted.
[filters.chat]#
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | false |
Drop in-game messages that begin with prefix instead of bridging them. |
prefix |
string | "!" |
The prefix that marks a message as game-only. |
This filter runs on the raw packet text before anything is sent, so a suppressed
message costs no network call. An empty or null prefix with enabled = true
forwards everything — an empty prefix never matches.
[bot_status]#
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled |
bool | true |
Keep the bot's activity line in sync with the player count. |
format |
string | "Online: {online}/{max}" |
Status template. Placeholders: {online}, {max}. |
Updates are coalesced. A mass reconnect produces one status push, not one per player — see Performance.
[account_linking]#
| Key | Type | Default | Range | Meaning |
|---|---|---|---|---|
enabled |
bool | true |
— | Allow /viscord discord link and Discord-side /link code. |
code_expiry |
int (seconds) | 300 |
60–600 | How long a generated code stays valid. |
Values outside 60–600 are corrected to the nearest bound when the file is loaded. The in-game message reports expiry in whole minutes, so a 90-second setting displays as 1 minute. The full handshake is in Account linking.
[discord_rate_limit]#
Sliding 60-second windows on the platform-side commands. A value of 0 disables
that bucket. Limits are read on every call, so a reload takes effect immediately.
| Key | Type | Default | Range | Meaning |
|---|---|---|---|---|
link_per_user_per_min |
int | 3 |
0–1000 | Per-user /link attempts per minute. |
link_global_per_min |
int | 30 |
0–10000 | Channel-wide /link attempts per minute. |
list_per_user_per_min |
int | 2 |
0–1000 | Per-user !list and /list calls per minute. |
list_global_per_min |
int | 20 |
0–10000 | Channel-wide !list and /list calls per minute. |
home_per_user_per_min |
int | 4 |
0–1000 | Per-user /home executions per minute. |
home_global_per_min |
int | 20 |
0–10000 | Channel-wide /home executions per minute. |
The six keys cover three buckets, not six independent limits. /link and /list
each have a per-user and a global cap; /home uses the home_* pair. Fluxer
!list calls share the list bucket under a single fluxer key because the
Fluxer message handler does not surface a stable per-user ID — the global cap
still applies, the per-user cap effectively does not.
A rate-limited /link is silent by design: no reply, one log line at info level
when general.debug is true. See
Commands for why.
[advanced]#
Two keys are declared by the config spec and written into every generated file. Neither is read by any Viscord code path in 5.2.1.
| Key | Type | Default | Range | Status |
|---|---|---|---|---|
queue_size |
int | 100 |
10–1000 | Generated and range-checked; no consumer. |
rate_limit |
int (ms) | 1000 |
100–5000 | Generated and range-checked; no consumer. |
Executor sizing, queueing, and back-pressure are fixed in code rather than configurable — see Performance. If you are upgrading from an older build and these keys are already in your file, leave them; they are harmless and will be preserved.
Types and coercion#
TOML integers arrive from NightConfig as Long. ConfigValue.get() inspects the
declared default type and coerces numbers to Integer, Long, or Double
before returning, and falls back to the default on a ClassCastException. That
means a hand-edited code_expiry = "300" (a string) yields the default rather
than a crash. It also means a wrong-typed value fails silently — if a number you
set appears to be ignored, check it is unquoted.
Upgrades and reload#
Two mechanisms keep an existing file valid:
ConfigSpec.correct()runs on every load. Missing keys are added with their defaults and out-of-range integers are clamped. This is what makes upgrading in place safe: a new key appears in your file rather than being invisible until you recreate it.applyDefaults()runs only when the file does not exist, writing the full commented file from scratch.
/viscord reload does the whole cycle off-thread: it shuts the current platform
connection down, resets the manager singleton so fresh platform objects are
created, re-reads the config directory, and re-initialises if
general.enabled is still true. Feedback is bounced back to the server thread.
If general.enabled is false at reload time you get
Viscord is disabled in config. and no connection.
A reload is a reconnect
Because the platform objects are rebuilt, reload also re-registers native Discord commands and re-sends the startup embed. A reload during a busy period is visible in the channel. Prefer it over a restart, but do not loop it.
Legacy JSON migration#
If config/viscord/viscord.json exists and viscord.toml does not, Viscord
migrates the old file: it writes a TOML file, copies across every key it knows
how to map (including renamed paths such as formats.discord_to_minecraft →
messages.discord_to_minecraft and linking.code_expiry_seconds →
account_linking.code_expiry), then renames the original to
viscord.json.backup. If migration throws, it logs the error and writes a
default TOML file instead, leaving the JSON in place.