Welcome to the Hangar Open Beta. Please report any issue you encounter on GitHub!
Avatar for jamesmblakely

Preserves UUID-keyed player progress when a Paper server temporarily switches to offline mode during auth/WAN outages.

Report IdentityBridge?

Release
1.0.0

jamesmblakely released IdentityBridge version 1.0.0 on Today 5:40 PM

First public release.

Features

  • Login continuity: while the server is in offline mode, known returning Java players are restored to their real online UUID at pre-login (via Paper's createProfileExact profile API), so they keep inventory, location, stats, advancements, and other UUID-keyed progress. Only genuine offline-style logins are rewritten — proxy/forwarded UUIDs are left untouched.
  • Known-player learning: authenticated online-mode logins are recorded (name → online UUID, last IP, timestamps), with careful name-capitalisation handling.
  • Temporary offline identities: unknown players seen during an outage are recorded for later migration (or can be blocked instead).
  • Safe data migration: /idb migrate and /idb migrate-uuid move UUID-keyed data to the correct online UUID — dry-run by default, with backups, missing-source reporting, an extensible file-location list, and a hard stop if the source or target player is currently online.
  • Lossless conflict resolution: when both UUIDs already hold data, migration unifies the two profiles instead of skipping or clobbering — stats counters are summed, advancements are unioned (earliest criterion timestamp wins), and an un-mergeable inventory (.dat) clash is decided in favour of the file with more play time — an exact play-time tie is broken by the number of completed advancement criteria — so the least progress is lost (--overwrite forces the source side). Nothing is ever discarded without a backup, and merged sources are folded in so a re-run never double-counts.
  • Auto-migrate on rejoin (migration.auto-migrate-on-rejoin, default on): when a player with temporary offline data logs back in, their data is folded into their real online UUID at pre-login — the only window where the files are not in use. Stats and advancements are merged losslessly; an un-mergeable inventory (.dat) clash is resolved in favour of the file with more play time (the displaced file is backed up first — never discarded without a backup), exactly like a manual migration without --overwrite. If a conflict can't be resolved safely (e.g. backups disabled), it is left untouched and logged for manual handling.
  • Optional auto-migration IP verification (migration.auto-migrate-require-ip-match, default off): when enabled, unattended rejoin migration only runs when the temporary outage record's IP matches the authenticated online rejoin IP. If the IP is missing or different, IdentityBridge captures the target UUID and leaves the record pending for manual /idb migrate review.
  • Startup cache backfill (async): on enable, IdentityBridge scans the UUID-keyed data files already on disk (migration.data-locations) and resolves any real online (Java) UUID it does not yet know to its current name via the Mojang session server, so players whose progress predates the plugin's install are still recognised if the server later goes offline. Runs entirely off the main thread (never blocks startup), is read-only, filters out offline-style and Bedrock UUIDs before contacting Mojang, throttles requests, backs off on rate limits, and records definitively unresolvable UUIDs in the identity store's ignored: list so they are looked up once and skipped thereafter. Custom templates whose {uuid} appears in the filename are discoverable too, including root-level entries like {uuid}.dat; per-player directory templates still work for explicit migrations but cannot be discovered by the startup directory scan.
  • Auth/service monitoring: periodic, detailed health checks of Mojang/Microsoft endpoints with an overall verdict, surfaced via /idb check and /idb status. Endpoints can declare their own healthy-statuses (defaults to any 2xx).
  • Advisory notifications: admins are advised (console + in-game) when auth looks down while online, or healthy while offline; monitoring.advise-on-degraded opts into acting on partial outages too. Advisory only — never edits config, switches mode, or restarts.
  • Identity cache management: /idb lookup, and a consistent action-first cache surface — /idb cache list [<java|offline|bedrock>], /idb cache add java, and /idb cache remove <java|offline|bedrock>. /idb status also shows the size of the startup backfill's ignored: skip-list (when non-empty), so it is visible without opening identities.yml.
  • Colour codes in kick messages: the configurable unknown-kick-message and ip-mismatch-kick-message honour legacy &-style colour/formatting codes (e.g. &c) and &#rrggbb hex colours; plain text passes through unchanged.
  • Optional Bedrock/Floodgate support: detects Bedrock players, records their identity keyed by XUID (with Floodgate UUID, linked Java UUID, and link state), and never rewrites Bedrock UUIDs. No hard dependency; fully functional on Java-only servers.
  • Full account-linking support (global and local): Floodgate's global (GeyserMC hosted) and local (database) linking are both supported transparently. Linked Bedrock players join under their linked Java UUID (already stable, never rewritten); links are read live for online players and resolved through Floodgate's link manager for offline ones. /idb lookup accepts a name, UUID, or XUID and cross-references the linked Java account; /idb status reports whether linking is global or local. A linked Java account is also recorded as a known player (bedrock.record-linked-java-account) so it gets Java-side continuity during an outage.
  • Consolidated storage: all identities live in one identities.yml with uniform java: / offline: / bedrock: sections, written atomically (temp file + atomic move) so a crash mid-write can never corrupt the cache.
  • Self-upgrading configuration: on future versions, new config keys merge in automatically with a backup of the previous file.
  • Privacy & IP-match hardening controls: privacy.store-player-ip can disable IP persistence entirely, and login-continuity.require-ip-match-strict makes the IP-match rule refuse logins it cannot verify (the default keeps the documented fail-open behaviour for backfilled/dynamic-IP players).

Safety & correctness

  • A manual /idb check run while monitoring.enabled is off shows the advisory that probe implied in the command output but never stores it — with no background monitor running, nothing could ever refresh or clear a stored advisory, so it would otherwise be repeated to every joining admin indefinitely. /idb check also reports the advisory computed by its own probe rather than re-reading shared state a concurrent scheduled check could have changed.
  • A scheduled auth check that was already running when /idb reload stopped/restarted monitoring can no longer publish a stale last-report or advisory state after reload; scheduled probes are generation-gated, and advisory persistence is still checked against the latest config snapshot when the probe finishes.
  • The offline-mode unknown-player path refuses to record anything if it is ever handed a non-offline-style UUID, instead of recording an unauthenticated connection as an authenticated known player (unreachable today thanks to the pre-login offline-UUID gate; defence in depth).
  • With login continuity disabled, a recognised player admitted under a temporary offline UUID is logged as a known player whose identity was not restored, instead of being mislabelled "Unknown player" — which would contradict /idb lookup for the same account mid-outage.
  • An authenticated online-mode login is only recorded as a known Java player - and only used as an auto-migration target - when it carries a real online-style (v4) UUID. Previously, with Floodgate installed but bedrock.enabled: false (so Bedrock detection was skipped), a Bedrock player's v0 UUID could be cached as a known Java identity, and a same-named temporary offline record could even be auto-migrated into that v0 UUID - the same wrong-destination hazard the join-time online-UUID capture already guarded against. The pre-login path now enforces the identical invariant.
  • The same v4-only and Java-username-shape invariants are enforced at the identity store's single entry point for automatically-learned identities (backfillKnown), which also covers the linked Java account recorded from a Bedrock join. That UUID/name pair comes from Floodgate's link data (a reflective API, and for local linking an admin-run database) rather than an authenticated login, so a non-v4 UUID or malformed name there - e.g. from a corrupt linking database or an unexpected Floodgate result - is refused instead of being cached as an offline-mode restore target. Only the explicit /idb cache add java --force admin override may store a non-v4 UUID.
  • The startup backfill's ignored: (no Java profile) list and the known-player cache are mutually exclusive: an authenticated join, backfill/link hit, or manual /idb cache add java for an ignored UUID clears the stale ignore marker, a cached known player is never marked ignored (e.g. a stale NOT_FOUND lookup racing an authenticated join), and on load the java: section wins over a contradictory hand-edited ignored: entry - so /idb status counts and identities.yml can never contradict each other.
  • Mojang profile responses are accepted only when their id is an online-style (v4) Java UUID and their name is shaped like a valid Java username. For UUID-to-profile startup backfill lookups, an unexpected HTTP 400 is treated as a transient error rather than a definitive "no profile" result, so the permanent ignored: list cannot be poisoned by a bad response to a syntactically valid UUID query.
  • Floodgate reflection now resolves API methods through a public declaring type (with an accessibility fallback), so a Floodgate build whose implementation classes are not public still yields Bedrock and link information instead of silently degrading to "no info".
  • A Floodgate-derived (v0) UUID remains a confirmed Bedrock signal even if a best-effort reflective isFloodgatePlayer call returns false, so stale/partial Floodgate API state cannot make a Bedrock login fall through as ordinary Java identity bookkeeping.
  • /idb cache list pages are ordered fully deterministically (last-seen ties are broken by name and UUID, Bedrock name ties by XUID), and /idb cache remove offline|bedrock reports a raced no-op removal the same way the java variant does instead of staying silent.
  • A Bedrock classification based only on the (spoofable) bedrock.username-prefix heuristic is no longer allowed to skip the login rules: in offline mode, a player whose name merely starts with the prefix now goes through the normal continuity/unknown handling — including block-unknown-during-outage — instead of being waved through unrecorded. Only a Floodgate API confirmation or a Floodgate-derived (v0) UUID, neither of which a Java client can forge, is treated as definitely Bedrock at login.
  • The join-time advisory notice reads the advisory state exactly once, so an advisory clearing concurrently can no longer produce a stray "no action needed" message styled as an advisory.
  • The last-known IP that require-ip-match trusts is only ever set from an authenticated online-mode login; an offline-mode restore updates last-seen but never the stored IP, so an unauthenticated offline connection can't poison the trust anchor and lock out the real player.
  • A manual cache refresh (/idb cache add java <name> [uuid]) keeps the record's stored authenticated IP, so refreshing a renamed player before an outage doesn't silently drop the require-ip-match anchor for exactly the account being prepared.
  • A manual Java cache entry with an explicit UUID is refused unless --force is supplied when Mojang can verify that the typed name currently belongs to a different UUID, preventing a likely typo from becoming a trusted offline-mode restore mapping after only a warning.
  • Java cache writes and name-based migrations now require real Java username shapes (1-16 ASCII letters, numbers, or underscores), and lookup skips live Mojang requests for strings that cannot be Java usernames. The floor is 1, not Mojang's current 3-character creation minimum, so grandfathered legacy accounts with shorter names keep login continuity. Explicit UUID migration remains available for unusual historical data.
  • An authenticated online join or explicit admin cache write now claims its current name even if a stale shared-name cache entry has the same timestamp or a clock-skewed future timestamp; the persisted last-seen time is moved just past the stale owner so the correct name owner survives a restart.
  • If a stale shared-name record has an impossible-to-beat saturated timestamp (Long.MAX_VALUE), a fresh authenticated join or explicit admin cache write now demotes only that competing name claim enough for the current evidence to own the name deterministically after restart.
  • The offline-mode restore path bumps a known player's last-seen monotonically (never backwards), so a restore during an outage cannot lower a name claim that an authenticated join had advanced past a clock-skewed shared-name competitor — which would otherwise let the stale competitor re-win the name when the index is rebuilt from disk after a restart mid-outage.
  • Name-based manual migration uses a captured resolved-online-uuid only from the selected temporary offline record, and refuses contradictory target evidence if that captured UUID differs from the current known-player cache for the same name. In that case it asks for an explicit --online-uuid after admin review instead of silently choosing one target source over the other.
  • An authenticated online join for a pending temporary-offline record now warns admins immediately if that record already points at a different captured online UUID, leaving the pending record unchanged instead of silently deferring the conflict until a later manual migration attempt.
  • /idb lookup no longer suppresses the live Mojang lookup for all-digit Java usernames just because the same string also matches a stored Bedrock XUID. Long raw XUIDs still skip the unnecessary Mojang request.
  • A clean applied /idb migrate <name> that resolved its target through Mojang now also remembers that Java identity for future offline-mode continuity, instead of waiting for a later online join or startup backfill.
  • The known-player name index reclaims the best remaining owner when a shared-name record is removed or renamed, instead of leaving a valid fallback UUID unreachable by name.
  • Bedrock records clear a stale linked-Java UUID when Floodgate explicitly reports the account is unlinked, but preserve the last known link when a lookup cannot determine link state.
  • /idb lookup distinguishes a Bedrock record that is linked but whose linked Java UUID has not been resolved yet (Floodgate reported the link, but the best-effort reflection could not read the Java UUID) from a genuinely unlinked one, instead of misreporting it as "not linked" while /idb cache list bedrock showed linked=true for the same record.
  • Bedrock lookup/removal by display name refuses ambiguous matches and asks for an XUID instead of picking an arbitrary record.
  • Migrations acquire a per-UUID lock, so a manual /idb migrate and an auto-migrate-on-rejoin that touch the same data are serialised rather than racing.
  • An unreadable stats/advancements file is preflighted before migration writes begin and reported as an error with both copies preserved, instead of being treated as empty, counted as zero play time for a .dat conflict, or deleted.
  • A zero-byte source stats/advancements file is now treated as empty/truncated and refused during the same preflight, instead of being merged as an empty JSON object and then removed.
  • A configured source or destination path that exists as a directory/non-file is now reported explicitly and left untouched rather than being treated as a missing or usable player-data file.
  • A zero-byte opaque source (.dat, .dat_old, or custom non-mergeable file) is now treated as empty/truncated and refused, so it can never be copied as player data or overwrite a valid target just because the source side has more stats progress.
  • Stat counter merges saturate at Minecraft's int ceiling on huge totals instead of overflowing.
  • Only .json files inside a directory named exactly stats or advancements are treated as mergeable; a custom data-location whose folder name merely contains "stats" is handled as an opaque file instead of having its JSON mis-merged as vanilla counters.
  • Atomic writes (identities.yml, config upgrades) clean up their temp file if the write fails, and a debounced save that races plugin disable falls back to saving inline instead of being dropped.
  • An identities.yml that exists but cannot be parsed (e.g. a hand-edit broke the YAML) is preserved as identities.yml.corrupt-<timestamp> with loud recovery instructions, and the plugin now refuses to continue with a guessed empty identity store. A broken identity cache can no longer silently alter outage behavior until the file is fixed, and a disable-time save after the failed load is explicitly refused so the broken original is not replaced by an empty generated store. That disable-time refusal is also reported accurately - as fail-closed protection pointing at the preserved corrupt file - rather than misdiagnosed as a file-permissions problem.
  • If an identity reload fails after the server already has a populated in-memory cache, the previous in-memory cache is restored before the fail-closed save guard is set, so a bad replacement load cannot silently empty the active outage-continuity state.
  • A config.yml that cannot be parsed is preserved as config.yml.corrupt-<timestamp> and the plugin refuses to continue with guessed/default settings, so a broken policy file cannot silently change outage behaviour.
  • The offline record is marked migrated for every clean outcome (including merge-only and kept-target), so a successful migration no longer lingers as "awaiting migration".
  • When the target .dat wins and no usable backup is available, --remove-source now retains the losing source file instead of discarding data that was not copied or merged anywhere.
  • A --remove-source deletion that fails after a successful opaque copy is reported as a retained-source warning (the copy is complete and the migration still counts as clean), not as a migration error that would falsely claim the file was left untouched and leave a fully-migrated record "awaiting migration".
  • A --remove-source deletion that fails after a kept-target decision now gets the same treatment: the source has already been backed up and the target remains authoritative, so the run reports a retained-source warning and still counts as clean instead of leaving the offline record pending solely because cleanup failed.
  • A migrated temporary-offline record is reopened as pending if that same offline UUID is used again in a later outage, so stale migrated state cannot hide newly-created offline data.
  • Temporary-offline records clear their stored outage IP when the current offline login has no address available, so optional auto-migration IP checks never trust stale IP evidence from a previous outage.
  • The low-level migrator now reports missing UUIDs or an empty data-location template list as structured migration errors instead of allowing an exception or ambiguous empty run.
  • migration.data-locations entries are now required to be relative paths, not absolute paths, even when an absolute path happens to point back inside the world folder. The startup backfill applies the same rule, and invalid absolute templates cannot influence .dat conflict scoring.
  • Name-based /idb migrate now rejects contradictory source selectors (--offline-uuid together with --offline-name), duplicate flags, and any non-v4 target (whether supplied by --online-uuid, cached from a forced java entry, or captured as old metadata); use /idb migrate-uuid when an arbitrary UUID-to-UUID move is intentional.
  • Server state needed by a migration is read on the main thread (the world folder is captured at enable; the online-player check is hopped to the main thread), never off-thread.
  • Backup directories are made unique, so repeated migrations of the same pair within one second can't overwrite each other's backups.
  • Admin cache add/remove commands warn when the in-memory change was made but identities.yml could not be saved, instead of reporting a clean success. Those paths now use the identity store's immediate persistence result directly, avoiding a redundant second save that could falsely warn after the first save had already succeeded.
  • Manual migration now reports post-migration identity-cache persistence failures with a dedicated metadata status instead of lumping them into file-migration errors, so command output no longer implies player-data files were left untouched when the data decision already completed.
  • Auth monitoring and Mojang lookup HTTP executors are bounded daemon pools, so repeated checks and lookups cannot create unbounded threads.
  • logging.verbose now includes the structured per-file migration result entries in the console for manual migrations and auto-migration on rejoin (rendered identically by both paths).
  • /idb check run with an empty monitoring.endpoints list now says explicitly that nothing was probed and why, instead of printing a bare Overall: UNKNOWN that reads like a failure.
  • /idb status reports Bedrock support that is switched off in config as disabled (bedrock.enabled: false) instead of not present — with the switch off the Floodgate API is deliberately left unbound, so the availability check alone cannot tell "disabled" from "not installed", and claiming Floodgate was absent could misdirect an admin diagnosing a server that does have it.
  • monitoring.request-timeout-seconds is documented as bounding every outbound request the plugin makes (Mojang lookups, the startup backfill, and Floodgate link resolution too), so it is clear the setting stays meaningful even with monitoring.enabled: false.
  • The shared path-containment helper now distinguishes file actions that must target a path below the world root from read-only scans that may deliberately scan the world root itself, with regression coverage for custom root-level startup-backfill templates such as {uuid}.dat.
  • UUID parsing (command arguments, identities.yml keys) accepts only ASCII hex digits. Non-ASCII Unicode digits — which Character.digit, and therefore UUID.fromString on modern JDKs, would quietly tolerate — are refused, keeping accepted input to the documented 0-9a-fA-F alphabet.
  • When a .dat conflict is decided by the advancement-criteria tie-break (play time equal on both sides), the per-file decision note cites the advancement counts instead of printing two identical play-time figures as if they justified the outcome.
  • Migration dry-runs preview source-file removal, not just writes: a --remove-source copy or kept-target cleanup, and the always-on fold-in of merged stats/advancements sources, are all announced in the preview entries (including the kept-target refusal when backups are disabled), so --apply never deletes a file the dry run did not mention.

Build

  • Built with Gradle (Kotlin DSL) via the bundled wrapper: ./gradlew buildbuild/libs/IdentityBridge-1.0.0.jar.
  • Gson is compile-only, relying on the copy already bundled in every Paper/Spigot server jar, so the release jar carries no duplicate third-party classes.
  • The jar is built reproducibly (fixed entry order, no entry timestamps), so a published release can be independently rebuilt and byte-compared, and its manifest carries Implementation-Title/Implementation-Version.
  • The MIT license text is bundled into the jar (META-INF/LICENSE), so the distributed binary carries its own license notice as the MIT terms expect.

Notes

  • Built against the Paper 1.20.1 API using only long-stable APIs; api-version: '1.20' is the compatibility floor, not a cap. Smoke-test on your exact Paper build before production use.
  • Compiled for Java 17 bytecode; run the server on the Java version required by your Paper build.

Information

Published onJuly 6, 2026
Downloads1

Platforms

Paper Paper (1.20.1-26.2)