DataRegistry is HauntedMC's shared read/write boundary for canonical player identity and DataRegistry-owned player metadata on Velocity and Paper.
It owns player creation, username updates, active identity state, connection metadata, language and nickname
preferences, playtime summaries, and name history. Feature plugins own their own tables and should reference
players by the stable scalar playerId.
- Velocity is the authoritative writer for joins, switches, disconnects, sessions, connection info, and probes.
- Paper prepares backend identity state and exposes the same read APIs to Paper features.
- DataProvider supplies database connections and ORM bootstrap.
- Production schemas must be migration-managed. Do not rely on Hibernate schema mutation in production.
- On Velocity startup, stale player presence from an unclean shutdown is reconciled before periodic flushing starts. Open sessions, visits, playtime segments, and online flags are closed from the last durable activity timestamp instead of from startup time.
- On Velocity shutdown, queued player lifecycle writes are drained before active players are persisted offline.
- Java 25
- Maven 3.8.6+
- DataProvider
2.1.4 - Velocity
4.0.0-SNAPSHOTand/or Paper26.1.2+
Start the server once to generate plugins/DataRegistry/config.yml, then review the database, feature,
privacy, playtime, service-registry, and platform sections.
Defaults and comments live in dataregistry-core/src/main/resources/config.yml. Missing supported keys are restored on load and stale keys are removed.
Depend only on dataregistry-api as provided:
<dependency>
<groupId>nl.hauntedmc.dataregistry</groupId>
<artifactId>dataregistry-api</artifactId>
<version>1.10.4</version>
<scope>provided</scope>
</dependency>Use DataRegistryApi#players() for player data:
DataRegistryApiProvider apiProvider = /* platform plugin instance */;
PlayerData players = apiProvider.getDataRegistry().players();
UUID uuid = player.getUniqueId(); // snapshot platform state before async continuations
players.whenReady(uuid).thenAccept(identity -> {
identity.ifPresent(value -> {
long playerId = value.playerId();
UUID canonicalUuid = value.uuid();
String username = value.username();
});
});dataregistry-apiis the only dependency for ProxyFeatures, ServerFeatures, and other consumers. It has no DataProvider, Hibernate/Jakarta Persistence, Velocity, or Paper dependency.dataregistry-coreowns entities, repositories, ORM wiring, lifecycle writers, recovery, and query execution. It is an implementation dependency of the platform modules, never a feature dependency.dataregistry-platform-velocityowns authoritative proxy lifecycle listeners, includingPlayerStatusListener;dataregistry-platform-paperprovides the Paper identity bridge.dataregistry-migrationscontains ordered schema migration resources; the default core schema mode isvalidate.dataregistry-testkitsuppliesFakeDataRegistryApi, immutable player fixtures, temporary IDs, and async failure simulation for feature contract tests.
DataRegistryApiProvider#getDataRegistry() returns DataRegistryApi, not the core runtime. Platform plugins
implement that provider capability; consumers can depend on dataregistry-api alone. There is deliberately no
public path from that type to an ORM context, entity, repository, lifecycle writer, or DataProvider handle.
Feature maintainers migrating from the former monolithic artifact should follow
DOWNSTREAM_MIGRATION.md. A dependency-coordinate change alone is not valid for a feature
that currently maps PlayerEntity in its own ORM model.
Use whenReady(uuid) in join paths. It completes when DataRegistry has finished the authoritative lifecycle
initialization for that player, including creation or username update if needed.
Use lookup-only methods outside lifecycle paths:
players.findIdentity(uuid),players.findIdentityByUsername(name), andplayers.findIdentity(playerId)players.findIdentityByIdentifier(identifier)for command input that may be a UUID or usernameplayers.findPlayerId(uuid)andplayers.findPlayerIdByIdentifier(identifier)players.findIdentities(lookups)for bulk identity resolutionplayers.findIdentitiesByUsernamePrefix(prefix, pageRequest)for cursor-based suggestions and staff toolingplayers.findActiveIdentityCached(uuid)only when cache-only behavior is explicitly acceptable
PlayerIdentity is immutable and standalone. It is safe to pass between feature layers and does not expose
Hibernate-managed state.
Use PlayerProfile when a feature needs a read snapshot of several DataRegistry-owned fields:
players.findProfileByIdentifier(input, 20).thenAccept(profileOpt -> profileOpt.ifPresent(profile -> {
PlayerIdentity identity = profile.identity();
Optional<String> nickname = profile.nickname();
List<PlayerNameHistoryEntry> names = profile.nameHistory();
}));Profiles may include language, nickname, connection, online, activity, playtime, and name-history data depending
on enabled modules and available rows. Missing optional feature data is represented as Optional.empty() or an
empty list. Profile projection is assembled by DataRegistry in one transaction for a consistent snapshot.
Use the specific facade methods when a full profile is unnecessary:
players.findLanguage(playerId)andplayers.saveLanguage(playerId, preference, effective)players.findNickname(playerId)andplayers.saveNickname(playerId, nickname)players.findConnection(playerId)players.findOnlinePlayers(limit)players.findActivity(playerId)players.findPlaytime(playerId)and leaderboard helpersplayers.findNameHistory(playerId, limit)players.findIdentitiesSharingLastIp(playerId)andplayers.findUsernamesSharingLastIp(playerId)players.findPlayerIdsByLastIpAddress(ip, excludePlayerId)andplayers.findUsernamesByLastIpAddress(ip, excludePlayerId)
Public persistence reads and DataRegistry-owned preference writes return CompletionStage and run on DataRegistry's
query executor with configured deadlines. Returned futures support cancellation when used as CompletableFuture.
Development thread checks warn when likely event threads request queries or block pending query stages. Completion
callbacks may run on DataRegistry worker or lifecycle threads, so snapshot Bukkit/Velocity state before starting async
work and schedule platform API work back onto the platform thread when required.
Downstream plugins must not create, update, or merge canonical player rows. They may write only through the narrow DataRegistry methods for DataRegistry-owned preferences such as language and nickname.
DataRegistry also exposes a process-local service catalog for feature-owned APIs. This lets enabled features share their own data and behavior without moving their tables into DataRegistry or forcing consumers to query another feature's ORM entities.
Feature plugins should publish narrow interfaces from their own lifecycle code:
FeatureServiceHandle handle = dataRegistry.featureServices().register(
"ServerFeatures",
"Vanish",
VanishAPI.class,
vanishService
);Consumers should resolve feature services by interface:
dataRegistry.featureServices()
.find(VanishAPI.class)
.ifPresent(vanish -> vanish.isVanished(playerId));Use find for optional integrations and require only when a feature cannot run without the dependency. Close the
returned FeatureServiceHandle during feature disable, or use the ServerFeatures/ProxyFeatures lifecycle API
manager, which publishes and unregisters services automatically.
The catalog is intentionally runtime-only. It does not provide cross-server RPC, cache persistence, or schema ownership. Exported interfaces should be stable, small, and expressed in scalar IDs or immutable value objects where possible.
Keep feature-owned data such as vanish, glow, nametags, friends, sanctions, client info, 2FA, voting, messaging,
and logs in the owning feature plugin. Do not move those records into DataRegistry. Prefer scalar player_id
references for new feature-owned tables and keep feature queries in the owning feature.
Feature-owned services are the supported sharing boundary for that data. For example, a messaging feature may ask
the Vanish feature whether a playerId is hidden, but it should not read or join the vanish table directly.
Authenticated GitHub Packages access may be required for private HauntedMC dependencies. Configure repository id
github in ~/.m2/settings.xml, then run:
mvn clean test packageBuild output:
dataregistry-api/target/dataregistry-api-*.jardataregistry-core/target/dataregistry-core-*.jardataregistry-platform-velocity/target/dataregistry-platform-velocity-*-bundled.jardataregistry-platform-paper/target/dataregistry-platform-paper-*-bundled.jar
Deploy the bundled platform JAR only. It embeds the platform's relocated core implementation while retaining the
public DataRegistryApi namespace. Do not deploy dataregistry-core as a separate server plugin and do not add it
as a dependency to feature plugins.
This project is licensed under the GNU Affero General Public License v3.0.