PlayerNotifications is a plugin for Paper Minecraft servers that stores
per-player notifications in a database and delivers them through pluggable, payload-typed
processors. Other plugins enqueue notifications against the shared NotificationService;
delivery back-ends (chat, dialogs, EssentialsX mail, Discord DMs) are
added as small feature modules without touching the core. Every notification also stays in the
player's inbox (/notifications) until they dismiss it.
Using the plugin as a player or server operator? See docs/USAGE.md.
- Paper 26.1.2+
- Java 25
- MariaDB (or MySQL) to store notifications
- EssentialsX — optional, only for the mail delivery module
- A Discord bot token — optional, only for the Discord DM module
From the repository root:
./gradlew :platform:paper-plugin:shadowJarInstall the JAR from platform/paper-plugin/build/libs/ whose name ends with -all.jar.
The optional Essentials mail delivery module is built separately and dropped into the plugin's
modules/ folder (see Feature modules):
./gradlew :platform:essentials-adapter:jarThe Discord DM module must be built as its shaded jar — it bundles its own relocated JDA, so the
plain jar output contains no Discord library at all:
./gradlew :platform:discord-adapter:shadowJar| Module | Role |
|---|---|
api |
Public API surface consumers program against (NotificationService, processors) |
core |
MyBatis/MariaDB persistence, DefaultNotificationService, and the delivery loop |
platform:paper-plugin |
The Paper plugin bootstrap |
platform:essentials-adapter |
Optional feature module: delivers notifications as Essentials mail (a delivery medium) |
platform:discord-adapter |
Optional feature module: delivers notifications as Discord DMs, and owns its own account-link schema |
On first run the plugin writes three YAML files to its data folder (parsed with Configurate):
database.yml — the database connection (the url omits the leading jdbc:):
url: mariadb://localhost:3306/player_notifications
username: ''
password: ''settings.yml — plugin behaviour:
prune-interval-seconds: 3600 # how often expired notifications are pruned, in seconds
default-media: [chat] # delivery methods for a player with no saved preference
deliver-on-join: true # push waiting notifications when a player logs in
join-delivery-delay-seconds: 3 # how long after joining to wait; 0 = immediately
inbox-page-size: 7 # inbox entries per page; clamped to 1-20categories.yml — the display grouping used by the "Notification types" preference screen. See
docs/USAGE.md for the full reference on all three files.
The schema is created and migrated automatically on enable.
- A notification has a key, a scheduled/expiry time, a priority, an audience (a set of player UUIDs), a data type string, and a string payload.
- Callers register, per data type, a payload
Classand aNotificationProcessorin theNotificationDataTypeRegistry(reached viaNotificationService#dataTypeRegistry). - Callers can instead register a
NotificationRenderer, which converts the payload once into a medium-neutral title and body. The framework then fans it out to whichever media that player prefers, each backed by aNotificationSink. Adding a medium requires no change to any payload. - The delivery loop resolves a player's due, unread notifications and, for each one, invokes the
processor registered for its data type — once per target. Each call returns a
NotificationDisposition(RETAINorMARK_SEEN);MARK_SEENstamps that target as read. Delivery marks read; it does not delete. The notification stays in the player's inbox until they dismiss it or it expires.
api and core are published to the network Maven repo — 1.0.0 is the current release:
repositories {
maven("https://maven.minecraftcitiesnetwork.com/releases")
}
dependencies {
// The API surface is all most consumers need; core is the persistence implementation.
compileOnly("io.github.md5sha256:player-notifications-api:1.0.0")
}Other plugins obtain the service from Bukkit's ServicesManager (no hard dependency required):
NotificationService notifications = getServer().getServicesManager()
.load(NotificationService.class);
// Enqueue a "welcome" notification; it reaches the player over whichever media they prefer.
notifications.enqueueNotification(new ResolvedNotification(
"welcome:" + playerId, // unique key
Instant.now(), // scheduled time
null, // no expiry
new NotificationTarget(List.of(playerId)),
"welcome", // data type, registered with a renderer
"{\"message\":\"Welcome to the server!\"}", // JSON payload
0), // priority
true); // overwrite an existing keyDelivery back-ends are feature modules — jars dropped into <data folder>/modules/, loaded at
runtime (via plugin-infrastructure).
Each jar contains a module-manifest.yml and an entry class implementing PluginModule:
module-name: essentials-mail-adapter
entry-class: io.github.md5sha256.playernotifications.essentials.EssentialsMailModule
author: md5sha256
expected-plugin-class: io.github.md5sha256.playernotifications.paper.PlayerNotificationsPlugin
reloadable: falseManifest keys are kebab-case — a camelCase key does not fail, it silently deserializes to null.
On initialize the module resolves the NotificationService and registers its processor, renderer
or sink. To build your own, apply the paper-adapter Gradle convention and model it on
platform/essentials-adapter.
./gradlew build— build and test everything../gradlew :core:test— run the persistence + delivery tests. These require a running Docker daemon; they spin up a real MariaDB container via Testcontainers../gradlew :platform:discord-adapter:testneeds Docker for the same reason../gradlew :platform:paper-plugin:runServer— launch a Paper 26.1.2 test server with the plugin loaded (needs a reachable MariaDB).
See CLAUDE.md for a deeper tour of the module layout, persistence design, and known gaps.