DataSynchronization
Cross-server player data sync for Paper, Purpur and Folia 1.21.4+
No dupes · No data loss · Folia-ready · Redis / RabbitMQ / SQL / MongoDB · History, rollback, audit and GUI tools
Overview
DataSynchronization keeps player data perfectly synchronised across an entire Minecraft network. It transfers inventories, ender chests, vitals, advancements, attributes, potion effects, locations, recipes, statistics, PersistentDataContainer (NBT) bytes and custom plugin data between servers.
Unlike legacy "last-write-wins" sync plugins, DataSynchronization is built to eliminate the dupe-and-data-loss class of bugs. Every save passes through a per-player FIFO queue and is fenced by a monotonic session-ownership token, so a write from a superseded peer is rejected before it can touch the database.
Requirements
- Server: Paper 1.21.4+, Purpur or Folia
- Java: 21 or newer
- Storage: H2, MariaDB, MySQL, PostgreSQL or MongoDB
- Recommended for networks: Redis
- Optional: RabbitMQ, Vault, PlaceholderAPI
Not supported: Bukkit and Spigot. The plugin relies on Paper-only APIs (native Adventure, PersistentDataContainer byte serialization, AsyncChatEvent, modern Paper metadata).
What gets synced
Every category can be enabled or disabled individually.
- Inventory: main inventory, armor, off-hand, ender chest
- Vitals: health, food, saturation, exhaustion, air, XP, level, score
- Effects: potion effects, attributes (max HP, speed, damage, reach, …)
- World state: location, bed spawn, gamemode, flight state, fly and walk speed
- Progression: advancements, recipes, statistics
- Plugin data: PersistentDataContainer / NBT byte data
- Custom modules: via the SyncModule API
- Per-world blacklist: exclude creative, build or minigame worlds
Core features
Storage and cluster
- H2, MariaDB, MySQL, PostgreSQL and MongoDB backends
- Sharded storage variants for large networks
- Redis pub/sub, RabbitMQ fanout, plugin messaging or auto-detect
- Session ownership with Redis SETNX, monotonic fencing tokens and TTL heartbeats
- Per-UUID FIFO write coalescing
- GZIP compression for 4-6x smaller stored rows
- Tiered cache (L1 in-memory + L2 Redis)
- Circuit breaker on the storage layer with automatic recovery
- Exponential-backoff retries on transient failures
- Async pipeline with priority queue and backpressure
Platform and runtime
- First-class Paper, Purpur and Folia support
- Folia entity-region scheduler used natively
- Native Adventure usage, no shaded BukkitAudiences bridge
- Inline-dispatch optimisation on hot paths
- bStats integration for setup-shape telemetry
Reliability and safety
- Per-player pause, persistent across restart and broadcast cluster-wide
- Auto-resume of duration-bound pauses via leader-elected sweep
- Optional kick-on-failed-sync with custom message
- Creative-mode inventory sync protection
- Confirmation prompts on destructive operations
- Command cooldowns for heavy operations
- SQL-injection-safe identifier validation
- Atomic export rename (write-temp-then-rename)
- Configurable 512 MiB import-size cap
- Parallel batch import with semaphore backpressure
History and rollback
- Per-player snapshot history with configurable rolling retention
- Pinned snapshots survive rolling cleanup
- Selective restore via
--only/--except - Preview diff before applying a restore
- Compare two snapshots of one player, or two players side by side
Live GUI tools
/ds view- paginated player browser with sort, search, snapshot navigation and compare mode/ds edit- in-game inventory, ender-chest and stats editor- Bossbar progress for save-all, export, import and reshard
Update notifications
- Asynchronous SpigotMC API check at startup and every six hours
- Result shown in
/ds version, the System block of/ds status, and as a one-shot click-to-SpigotMC notice for operators on join - Five-second timeout, never blocks the main thread, polite User-Agent
- Disable with
update-checker.enabled: falseon networks with strict outbound policies
Admin tooling
Operations is a first-class feature surface, not an afterthought.
- Doctor: storage, messaging, cache, circuit breaker and config-smell checks
- Metrics: uptime, save and load counts, retries, queue depth, cache hit rate
- Benchmark: synthetic save and load cycles with min / p50 / p95 / max
- Round-trip test: per-player serializer self-check
- Stats: row count, average and largest sizes, top-N heaviest players
- Errors view: in-game WARNING / SEVERE log viewer, cluster-synced
- Watch: live-tail sync events for selected players
- Whoami: own identity, permissions and session state
- Whois: forensic combined view of a player
- Search: grep across audit and SEVERE-error buffers
- Audit log: 25 tracked action types, filterable and paginated
- Debug: pipeline, session and sharding internals with live refresh
- Import / export: streaming JSON with dry-run and selective scope
Modules and API
Third-party plugins can synchronise their own data through the SyncModule API without forking DataSynchronization.
- Register custom capture and apply logic
- Per-module metrics
- Per-module status sections
- Per-module GUI icons in
/ds view - Per-module subcommands under
/ds <module-id> - Per-module scheduled tasks
- Per-module PlaceholderAPI placeholders
- Data versioning with migration hooks
- Merge strategies for UUID merges
- Atomic single-field patches without load-mutate-save
Bundled modules: Vault economy and Advancements (with recipes and statistics toggles).
Translations
Six languages out of the box: English, Deutsch, Espanol, Portugues brasileiro, Francais, Polski.
- MiniMessage formatting throughout (colours, gradients, hover and click events)
- Live reload via WatchService - edit a YAML, save, see the change immediately
- Four-layer fallback chain: on-disk active, on-disk default, JAR-bundled active, JAR-bundled default
- Versioned bundles with safe auto-upgrade: outdated on-disk files are backed up before replacement
- Drop-in YAML support for any additional ISO-639 language
Comparison
Compared to HuskSync
- Monotonic fencing tokens prevent late, outdated writes
- Module API for arbitrary plugin data, including status and GUI surfaces
- In-game GUI editor, not just read-only viewing
- Storage sharding with offline migration tooling
- Snapshot pinning and selective restore
- Folia-first scheduler design
- Six bundled languages, live-reloadable
- Doctor command with infrastructure and config diagnostics
- Redis, RabbitMQ and plugin-messaging support (not just Redis)
- Tiered cache reduces Redis call volume by approximately 20x
- GZIP-compressed payloads
- Cluster-wide auto-purge with leader election
- PersistentDataContainer sync as a first-class data type
Compared to legacy sync plugins
- Modern database support across SQL and NoSQL
- Async pipeline with backpressure
- Circuit breaker and exponential-backoff retries
- Session fencing
- Streaming import and export with progress reporting
- History, rollback and audit trail
- MiniMessage translations
- SQL-injection-safe identifier handling
Quickstart
Single Paper server
Code:
storage-method: h2
cache:
type: memory
messaging-service: none
Drop the jar into
plugins/ and start the server.Multi-server network
Code:
storage-method: mariadb
server-name: "survival-01"
cache:
type: redis
messaging-service: redis
redis:
address: redis.internal:6379
password: "..."
data:
address: maria.internal:3306
database: datasync
username: datasync
password: "..."
All servers share Redis and MariaDB. Sessions, cache and messaging are Redis-backed. Each server gets its own
server-name.Recommendations
Storage backend
- Single server, dev, CI: H2
- Multi-server SQL setup: MariaDB or PostgreSQL
- Document-based stack: MongoDB
- 2,000+ concurrent players: any of the above with sharding enabled
Redis
Not required for single-server setups. Strongly recommended for multi-server networks: Redis unlocks atomic session claims, a shared cache across servers and sub-millisecond pub/sub invalidation.
Sharding
Enable only when a single database is no longer sufficient. Most networks below 2,000 concurrent players run comfortably on a single MariaDB or PostgreSQL instance.
What not to sync
- Disable
statisticson networks with long-lived accounts (those maps can grow into thousands of entries per player) - Disable
persistentDataContainerif no plugin actually writes PDC bytes you need to share
FAQ
Does it work on Folia?
Yes. Capture runs on the player's entity-region thread via the platform scheduler; everything else runs off-thread.
Why not Spigot or Bukkit?
The plugin uses Paper-only APIs (native Adventure, PersistentDataContainer byte serialization, AsyncChatEvent, modern Paper metadata) without Spigot equivalents.
What happens if the database goes down?
The circuit breaker opens after a configurable number of consecutive failures, stops hammering the database, and resumes automatically when it recovers.
What happens if a server crashes mid-session?
The session-ownership claim expires via TTL. Another server can then acquire the claim and load the player. Late writes from the crashed peer are rejected by the fencing token.
Can I sync my custom plugin's data?
Yes. Implement
SyncModule (or subclass AbstractSyncModule) and register it through the API or auto-discovery.Can I restore only part of a snapshot?
Yes. Use
/ds restore <player> <#> --only inventory,enderchest or --except statistics.How large are stored rows?
With GZIP enabled (default), typical late-game players sit in the 10-40 KiB range.
Can I add my own language?
Copy
translations/en.yml to e.g. it.yml, translate, save. The file watcher reloads it live.Will translations break when I update the plugin?
No. Each bundled YAML carries a
meta.version; outdated on-disk files are backed up to <name>.yml<random-suffix> and replaced with the new bundled version automatically.Commands
Main command:
/datasync (alias /ds).
Code:
/ds status [--section <name>]
/ds reload
/ds version
/ds metrics [--reset]
/ds doctor [--quiet]
/ds debug <pipeline|session|sharding> [player] [--watch]
Code:
/ds saveall [--filter <regex>]
/ds saveandkick <player|*> [--reason ...]
/ds forcesync <player>
/ds lookup <player> [--section <name>] [--json]
/ds view [player]
/ds view-confirm
/ds edit <player> inventory|enderchest|stats
/ds compare <p1> <p2> [--section <a,b>] [page]
/ds pause <player|--list|--all> [--duration <Nm|Nh|Nd>] [reason]
/ds resume <player|--all> [confirm]
Code:
/ds snapshot <player> [--note ...]
/ds history <player> [--contains ...] [--since 1h|7d] [page|pin <#>|unpin <#>]
/ds diff <player> <#1> <#2> [--section <a,b>] [page]
/ds restore <player> <#> [--only|--except types] [--preview] [--reason ...] [confirm]
Code:
/ds purge <player> [--reason ...] [--dry-run] [confirm]
/ds purge --inactive <days> [--reason ...] [--dry-run] [confirm]
/ds reset <player> --type <types> [--reason ...] [confirm]
/ds move <old-uuid> <new-uuid> [--dry-run] [confirm]
/ds export <file> [--inactive <days>] [--with-history] [--with-audits] [--with-errors] [--all]
/ds import <file> [--dry-run] [--skip-players] [--skip-history] [--skip-audits] [confirm]
/ds reshard <new-shard-count> [--export <dir>]
Code:
/ds cache info
/ds cache clear [player|confirm]
/ds cache refresh [player]
Code:
/ds audit [player] [--action a,b,c] [--by <name>] [--server <name>] [--contains <text>] [--since 1h|7d] [--until 1h|7d] [--export] [page]
/ds errors [--level WARNING|SEVERE] [--contains <text>] [--since 1h|7d] [page]
/ds watch <player|off>
/ds stats
/ds benchmark [N] [--concurrent <K>]
/ds test <player> [--count <N>]
/ds whoami
/ds whois <player>
/ds search <query> [--sources audit,errors] [--limit N] [page]
Code:
/ds module list [--enabled|--disabled] [page]
/ds module info <id>
/ds module enable <id>
/ds module disable <id>
/ds module reload [<id>] [--only <ids>]
/ds module export <id> <file>
/ds module import <id> <file>
Code:
/ds translations info
/ds translations reload
/ds translations set <lang>
Permissions
Parent grant (operators only):
Code:
datasync.admin
Self-service permission, default true:
Code:
datasync.command.whoami
Code:
datasync.command.status
datasync.command.reload
datasync.command.saveall
datasync.command.saveandkick
datasync.command.lookup
datasync.command.purge
datasync.command.forcesync
datasync.command.history
datasync.command.restore
datasync.command.export
datasync.command.import
datasync.command.cache
datasync.command.audit
datasync.command.diff
datasync.command.module
datasync.command.snapshot
datasync.command.doctor
datasync.command.metrics
datasync.command.version
datasync.command.compare
datasync.command.test
datasync.command.errors
datasync.command.watch
datasync.command.reset
datasync.command.benchmark
datasync.command.stats
datasync.command.move
datasync.command.view
datasync.command.view-confirm
datasync.command.edit
datasync.command.debug
datasync.command.reshard
datasync.command.pause
datasync.command.resume
datasync.command.translations
datasync.command.whois
datasync.command.search
Power-user permission, dupe-capable:
Code:
datasync.command.view.copy
Warning: grant
datasync.command.view.copy deliberately. It enables shift-click item copying inside /ds view.Developer API
The
api module is published to https://repo.tmp0.cc/releases. Coordinates:
Code:
de.squarecodefx:datasynchronization-api:<version>
Capabilities:
- Load and save player data
- Access and restore snapshots
- Atomic single-field patches
- Register custom SyncModules
- Publish and subscribe to cross-server custom messages
- Inspect metrics and runtime configuration
- Use typed SyncKeys for module data without boilerplate
Code:
de.squarecodefx.datasynchronization.api
de.squarecodefx.datasynchronization.api.annotation
de.squarecodefx.datasynchronization.api.data
de.squarecodefx.datasynchronization.api.event
de.squarecodefx.datasynchronization.api.exception
de.squarecodefx.datasynchronization.api.module
Code:
DataSynchronizationAPI api = DataSynchronizationProvider.get();
Code:
api.getPlayerData(uuid);
api.getPlayerSnapshots(uuid, 10);
api.forceSyncPlayer(uuid);
api.patchPlayerField(uuid, "health", 20.0);
api.setSyncPaused(uuid, true);
api.publishCustomMessage("channel", "payload");
