MySQL
Overview
The MySQL extension is the bridge that lets several TypeWriter servers share the same data. When you enable it, the facts (player progress) and the artifacts (data files created by other extensions) are no longer kept in files under plugins/Typewriter/. They are stored in a MySQL or PostgreSQL database that every server of your network reads and writes.
It also owns the optional Redis (or Valkey) connection. Redis carries instant change notifications, the "which server is this player on" registry, and the messages used by the social extensions (Friend, Party, Guild) and by Ghost. Several other BTC extensions read their settings, game mode and server name from this extension.
This extension has no entries: you configure it with a file and, optionally, from the BTC web panel.
Features
- Database storage — facts and artifacts in MySQL 8 or PostgreSQL, with automatic table creation and a connection pool.
- Safe by default — when shared storage is requested and cannot be reached, the server stops instead of silently playing on local files.
- Game modes — separate data per game mode (survival, skyblock, minigames) in the same database.
- Cross-server sync — changes are picked up by other servers within seconds, instantly with Redis.
- Redis and Valkey — pub/sub messaging, network-wide player presence and player transfers through the proxy.
- Per-extension storage rules — choose which extensions use the database, and whether their data is per profile and shared across game modes.
- First-start migration — existing facts and asset files are copied to the database when it is empty.
- Server registry — each server reports its status (TPS, players, memory, extensions) every 30 seconds.
- Secrets from the environment — any setting can be read from an environment variable.
- Administration panel support — server health, game modes, data export and table browsing for the BTC web panel.
Installation
- Create an empty database and a user for it on MySQL 8 or later, or on PostgreSQL. The user needs the right to create and alter tables.
- Optional, for several servers: run a Redis or Valkey server that every backend can reach.
- Put the MySQL JAR in
plugins/Typewriter/extensions/and restart once. The fileplugins/Typewriter/mysql/config.ymlis created. - Edit the file as described in Getting started, then restart. The database drivers, connection pool and Redis client are bundled with the extension.
Getting started
Move a server to a shared database:
- Stop the server and open
plugins/Typewriter/mysql/config.yml. - Under
mysql, fillhost,port,database,usernameandpassword. For PostgreSQL also adddatabase-type: "POSTGRESQL"and use port5432. - Set
mysql.game-mode.currentto the game mode of this server, for examplesurvival. Servers that must share facts need the same value. - Set
mysql.enabled: true. - For a network, enable Redis:
redis.enabled: true, withhost,portandpassword, a uniqueredis.server-nameper server and the sameredis.group-idon all servers that belong together. - Start the server. The log shows "Database storage initialized". Existing facts and files are copied to the database when it is still empty (see Migration).
- Repeat on the other servers with their own
server-name.
enabled on purposeWith mysql.enabled: false a server stores everything in its own files. That is fine for one permanent server, but if its directory is recreated from a template when it starts, the data is written to a folder that will not exist next time, and nothing warns you. With mysql.enabled: true, a server that cannot reach the database shuts down rather than serve players on local files.
How it works
When the extension starts it replaces TypeWriter's file storage with the database storage, before any other extension reads its data. TypeWriter keeps facts in memory and writes the changes to the database in small batches. Other servers read the changes they have not seen yet by polling the database, and Redis tells them immediately when something changed. When a player quits or is kicked, their pending facts are written at once.
The database stores only what changed. A deleted fact is stored as a row with value 0 (a tombstone), so other servers learn about the deletion instead of re-saving the old value. Tombstones older than fact-tombstone-retention-days are purged at start.
Configuration
The file is plugins/Typewriter/mysql/config.yml. Most sections are optional: any missing key takes its default.
Database
| Key | Default | Meaning |
|---|---|---|
mysql.enabled | false | Use the database. When true, the server stops if the database is unreachable or a setting is incomplete. |
mysql.database-type | MYSQL | MYSQL or POSTGRESQL. Not in the generated file, add it if needed. |
mysql.host, mysql.port | localhost, 3306 | Database address. Use 5432 for PostgreSQL. |
mysql.database | typewriter | Database name. |
mysql.username, mysql.password | typewriter, empty | Credentials. |
mysql.use-ssl | false | Use SSL (require for PostgreSQL). |
mysql.allow-public-key-retrieval | true | MySQL only. |
mysql.table-prefix | btc_ | Prefix of the tables created by this extension. Characters outside letters, digits and _ become _, and the prefix is lowercased. |
mysql.migrate-from-files | true | Copy existing asset files to the database when the asset table is empty. |
mysql.fact-tombstone-retention-days | 30 | Days before deletion rows are purged. Must be longer than the time any server stays offline. |
mysql.sync-interval-seconds | 30 | Read but not used in this version: the polling interval is set by TypeWriter itself. |
mysql.pool.* | see below | Connection pool. |
mysql.game-mode.current | default | Game mode of this server. |
The pool keys are maximum-pool-size (10), minimum-idle (2), connection-timeout (30000 ms), max-lifetime (1800000 ms), validation-timeout (5000 ms) and keepalive-time (0, disabled).
Environment variables
Any value that is exactly ${env:NAME} is read from the environment variable NAME when the file is loaded:
mysql:
password: ${env:BTC_TYPEWRITER_DB_PASSWORD}
A numeric or boolean value (for example a port) is read with the right type. An unset or empty variable stops the server with a message naming the variable. Only whole values are supported, not text that merely contains ${env:...}.
Redis and Valkey
| Key | Default | Meaning |
|---|---|---|
redis.enabled | false | Connect to Redis or Valkey. Works even when mysql.enabled is false. |
redis.type | redis | redis or valkey. Both use the same protocol. |
redis.host, redis.port | localhost, 6379 | Server address. |
redis.username, redis.password | empty | Credentials. The username default or an empty one is treated as no username. |
redis.channel | typewriter:events | Pub/sub channel shared by all servers. |
redis.server-name | survival in code, server1 in the generated file | Unique name of this server. It is also the proxy server name used for transfers. |
redis.group-id | empty | Network group of this server. Letters, digits, ., _, -, up to 64 characters. Empty means isolated. |
The group-id scopes every feature that talks between servers: social messages, ghosts and network rosters only travel between servers with the same non-empty group id. Servers with a different group do not see each other.
Per-extension storage
Under mysql.extensions.<name>, each entry has three keys:
enabled— whether the extension stores its own data in the database.profile-mode—shared(one set of data for all profiles) orper-profile.network-mode—shared(the same data in every game mode) orisolated(one set per game mode).
The generated file lists defaults for profiles, cosmetics, party, battlepass, translate and others. These settings are read by the extensions that support them, for example Profiles. They do not affect TypeWriter's own facts and artifacts, which always go to the database when mysql.enabled is true.
Settings are resolved in this order: the entry in config.yml, then the row of the table <prefix>game_mode_extensions for the current game mode. At start, every loaded extension gets a row there, disabled, waiting for an administrator to enable it. Key names are normalised: TypeWriter-ProfilesExtension and profiles are the same, and friends is an alias of party.
Profiles section
The profiles section of the same file configures the Profiles extension: enabled, max-profiles, default-profile-name, save-statistics, save-inventory, save-ender-chest, require-permission, auto-create-default, force-profile-selection, switch-cooldown-seconds, coop-supported and creation-menu-entry-id. See that page for their meaning.
What is stored
| Table | Content |
|---|---|
<prefix>facts | Facts of every player and group: entry id, group id, game mode, value, last update. |
<prefix>assets | Artifacts and other assets (text or binary), by path and game mode. |
<prefix>game_modes | Game modes known to the network. |
<prefix>game_mode_extensions | Storage rules per game mode and extension. |
<prefix>server_extensions, <prefix>extension_entry_filters | Synchronisation settings per server and per entry. |
servers | Registry of running servers. Its name has no prefix. |
Other BTC extensions create their own tables in the same database, for example btc_guild_bank for the Guild bank and typewriter_profiles for Profiles. Table and column names are created automatically and missing columns are added at start.
Every fact and asset row carries a game mode. A server only reads and writes the rows of its own mysql.game-mode.current (and rows marked global), so two game modes never share facts or artifacts even in the same database.
Cross-server sync
Data. Without Redis, servers still converge: each one polls the database for changes made by the others. With Redis, a server that writes facts publishes a short notice, and the others sync immediately. Run all servers in the same time zone, because fact timestamps are stored in the local time of each server.
Social messages. Friend, Party and Guild publish their changes as JSON messages on the Redis channel. A server ignores its own messages. They are sent only when Redis is connected and redis.group-id is not empty.
Presence. Each server writes presence:<player name> in Redis for every online player, with a 120 second expiry refreshed every 50 seconds and removed when the player quits. Features such as "teleport to a friend" read it to find the player's server.
Transfers. To move a player to another server, the extension asks the proxy through the standard BungeeCord plugin channel, and for cluster-wide moves through the btc:bridge channel understood by the BTC proxy. The redis.server-name of each server must match the server name known to the proxy.
Reconnection. If Redis drops, publishing tries to reconnect, and the subscriber retries up to 10 times with a growing delay (5 seconds up to 5 minutes).
Migration
The first time a game mode is started with mysql.enabled: true:
- If the fact table holds no facts for this game mode, the facts currently in memory (from the local files) are copied to the database. Otherwise the facts are loaded from the database and the local ones are ignored.
- If
mysql.migrate-from-filesistrueand the asset table is empty for this game mode, the files ofplugins/Typewriter/assets/are copied to it.
Keep a backup of plugins/Typewriter/ before the first start. Going back to files means setting mysql.enabled: false: the database content is not copied back.
Administration panel
The extension answers management requests from the BTC web panel: list servers and their health (TPS, players, memory, installed extensions, Redis latency, pool statistics), create and delete game modes, enable extensions per game mode, export, import and migrate the data of a game mode, browse and edit the storage tables, and read recent database logs. Each request checks a permission of the person using the panel and is rate limited.
Permissions
These permissions are checked for the web panel user. They are not registered with a default, so grant them to administrators.
| Permission | Grants |
|---|---|
typewriter.admin.server.list | View servers, game modes, storage, tables and discovered extensions. |
typewriter.admin.server.health | View health checks and database logs. |
typewriter.admin.server.deregister | Remove a server from the registry. |
typewriter.admin.gamemode.create | Create and delete game modes. |
typewriter.admin.gamemode.configure | Change extension settings, entry filters and storage overrides, import, export and migrate data, edit table rows. |
Operations
- Reload. Publishing
config_updateon the Redis channel makes every server reloadmysql/config.ymland register new extensions. Normal operation reads the file once at start. - Server registry. The
serverstable is refreshed every 30 seconds. Entries older than 60 seconds are treated as offline and removed at start, and a server removes itself when the JVM stops. - Deadlocks. Fact batches are retried up to 3 times when MySQL reports a deadlock.
- Backups. Back up the
<prefix>factsand<prefix>assetstables. Because a deletion is a row with value0, a restored backup older than the tombstone retention can bring deleted facts back. - Updates. Update the JAR on all servers in the same restart window. The schema is upgraded in place at start.
Troubleshooting
The server stops at start with "Refusing to serve players on local file storage." mysql.enabled is true and the database is unreachable or a value is incomplete. Read the line before it: the SEVERE message names user@host:port/database and the error. Fix the credentials, the network or the database, or set mysql.enabled: false if this server should use files.
"Configuration incomplete: environment variable(s) X are referenced ... but are not set." A ${env:X} value has no matching, non-empty environment variable in the server process. Set it in the service definition, not only in your shell.
Facts are not shared between two servers. Check that both have mysql.enabled: true, the same database, and the same mysql.game-mode.current. Different game modes store separate facts. Without Redis a change can take up to the polling interval to appear.
Friends, parties, guilds or ghosts do not cross servers. Redis must be enabled and connected, redis.group-id must be set to the same non-empty value on every server, and redis.server-name must be unique per server. With an empty group id, social messages are not sent.
The log shows "Failed to connect to Redis". Check host, port, username and password. A Redis requirepass password needs redis.password. The extension keeps working without Redis, using polling only.
An extension does not store its data in the database. Extensions discovered at start are registered disabled. Enable the extension under mysql.extensions in config.yml, or in the administration panel, then restart.
Facts of a deleted player or a deleted fact come back. A server was offline for longer than fact-tombstone-retention-days and re-saved old data. Raise the retention and avoid restoring old copies of a server.
"HikariPool ... connection is not available" or timeouts. The pool is exhausted. Raise mysql.pool.maximum-pool-size (and the database's own connection limit), and check for slow queries on the database server.
Timestamps look wrong or changes are missed. Servers sharing a database run in different time zones. Align them.