Welcome to the Hangar Open Beta. Please report any issue you encounter on GitHub!
Preserves UUID-keyed player progress when a Paper server temporarily switches to offline mode during auth/WAN outages.
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
createProfileExactprofile 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 migrateand/idb migrate-uuidmove 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 —
statscounters are summed,advancementsare 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 (--overwriteforces 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 migratereview. - 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'signored: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 checkand/idb status. Endpoints can declare their ownhealthy-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-degradedopts 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 statusalso shows the size of the startup backfill'signored:skip-list (when non-empty), so it is visible without opening identities.yml. - Colour codes in kick messages: the configurable
unknown-kick-messageandip-mismatch-kick-messagehonour legacy&-style colour/formatting codes (e.g.&c) and&#rrggbbhex 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 lookupaccepts a name, UUID, or XUID and cross-references the linked Java account;/idb statusreports 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.ymlwith uniformjava:/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-ipcan disable IP persistence entirely, andlogin-continuity.require-ip-match-strictmakes 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 checkrun whilemonitoring.enabledis 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 checkalso 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 reloadstopped/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 lookupfor 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 --forceadmin 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 javafor 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 thejava:section wins over a contradictory hand-editedignored:entry - so/idb statuscounts andidentities.ymlcan never contradict each other. - Mojang profile responses are accepted only when their
idis an online-style (v4) Java UUID and theirnameis 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 permanentignored: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
isFloodgatePlayercall returns false, so stale/partial Floodgate API state cannot make a Bedrock login fall through as ordinary Java identity bookkeeping. /idb cache listpages are ordered fully deterministically (last-seen ties are broken by name and UUID, Bedrock name ties by XUID), and/idb cache remove offline|bedrockreports 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-prefixheuristic 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 — includingblock-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-matchtrusts 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 therequire-ip-matchanchor for exactly the account being prepared. - A manual Java cache entry with an explicit UUID is refused unless
--forceis 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-uuidonly 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-uuidafter 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 lookupno 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 lookupdistinguishes 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 bedrockshowedlinked=truefor 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 migrateand an auto-migrate-on-rejoin that touch the same data are serialised rather than racing. - An unreadable
stats/advancementsfile 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.datconflict, or deleted. - A zero-byte source
stats/advancementsfile 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
.jsonfiles inside a directory named exactlystatsoradvancementsare 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.ymlthat exists but cannot be parsed (e.g. a hand-edit broke the YAML) is preserved asidentities.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.ymlthat cannot be parsed is preserved asconfig.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
.datwins and no usable backup is available,--remove-sourcenow retains the losing source file instead of discarding data that was not copied or merged anywhere. - A
--remove-sourcedeletion 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-sourcedeletion 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-locationsentries 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.datconflict scoring.- Name-based
/idb migratenow rejects contradictory source selectors (--offline-uuidtogether 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-uuidwhen 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.ymlcould 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.verbosenow 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 checkrun with an emptymonitoring.endpointslist now says explicitly that nothing was probed and why, instead of printing a bareOverall: UNKNOWNthat reads like a failure./idb statusreports Bedrock support that is switched off in config asdisabled (bedrock.enabled: false)instead ofnot 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-secondsis 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 withmonitoring.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 thereforeUUID.fromStringon modern JDKs, would quietly tolerate — are refused, keeping accepted input to the documented0-9a-fA-Falphabet. - When a
.datconflict 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-sourcecopy or kept-target cleanup, and the always-on fold-in of mergedstats/advancementssources, are all announced in the preview entries (including the kept-target refusal when backups are disabled), so--applynever deletes a file the dry run did not mention.
Build
- Built with Gradle (Kotlin DSL) via the bundled wrapper:
./gradlew build→build/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 on | July 6, 2026 |
|---|---|
| Downloads | 1 |
Platforms
Paper (1.20.1-26.2)