Let your ideas bloom.
One elegant framework. Every generation of Minecraft development.
Write a plugin once with Bloom's menus, items, messages, config and storage. Bloom runs it on Spigot 1.8 and on Paper 26, and handles the differences behind version adapters.
The same jar on every server you support
Bukkit's API changed shape across a decade of releases: online players became a collection, inventory views became interfaces, materials were renamed, and text moved to components. Bloom compiles your plugin against the oldest API, detects the server at startup, and loads the adapter that speaks its dialect.
Switch the server on the right to see what Bloom reports when the same plugin starts.
[INFO] [ShopPlugin] Bloom 0.1.0 · Spigot 1.8.8 (Java 1.8.0_472) · adapter: legacy (Bukkit 1.7.10-1.8.9)[INFO] [ShopPlugin] capabilities: ACTION_BAR, TITLES, ITEM_NBT_TAGS, ITEM_FLAGS, UNBREAKABLE_ITEMS, LEGACY_DATA_VALUES[INFO] [ShopPlugin] unavailable: ADVENTURE_TEXT, MINIMESSAGE, HEX_COLORS, TITLE_TIMINGS, BOSS_BAR, COMPONENT_INVENTORY_TITLES, PERSISTENT_DATA_CONTAINER, CUSTOM_MODEL_DATA
Legacy capabilities come from probes run at startup; this is the list when every probe succeeds.
A shop in thirty lines
Items use modern names everywhere: on 1.8, COOKED_BEEF and LIME_WOOL resolve to their legacy materials and data values. The action bar is sent where the server supports it and skipped with one warning where it does not.
This code is compiled by Bloom's build on every change, so it matches the current API.
public final class ShopPlugin extends JavaPlugin {
private Bloom bloom;
@Override
public void onEnable() {
bloom = Bloom.enable(this); // detects 1.7.10 ... 26.x and loads the matching adapter
Items items = Items.of(bloom);
Menu shop = Menu.builder()
.title("&dBlossom Shop")
.rows(3)
.button(
13,
Button.of(items.builder("COOKED_BEEF").name("&6Feed me").build(), click -> {
click.player().setFoodLevel(20);
bloom.messages().actionBar(click.player(), "&aYum!");
click.close();
}))
.build();
bloom.events()
.subscribe(
PlayerJoinEvent.class,
event -> bloom.scheduler()
.syncLater(() -> Menus.of(bloom).open(event.getPlayer(), shop), 20));
}
@Override
public void onDisable() {
bloom.shutdown(); // closes menus, cancels tasks, unregisters listeners
}
}Compatibility
Supported ranges, and how each one has been verified so far. Bloom labels a version tested only after it has actually been run.
| Minecraft | Server | Java | Bloom | Verified by |
|---|---|---|---|---|
| 1.7.10 | CraftBukkit, Spigot, PaperSpigot | 8 | Supportedlegacy adapter | tested-real-server PaperSpigot 1.7.10, unit-testsNo titles or action bars (1.7 protocol). |
| 1.8 - 1.8.9 | CraftBukkit, Spigot, PaperSpigot | 8 | Supportedlegacy adapter | tested-real-server PaperSpigot 1.8.8, compile-verified 1.8.8, unit-testsTitles via Spigot API, action bars via reflection. |
| 1.9 - 1.21.0 | any | - | Not supported | —No adapter yet. Bloom refuses to start with an explanation. |
| 1.21.1 - 1.21.11 | Paper and forks | 21 | Supportedmodern adapter | tested-real-server Paper 1.21.11, compile-verified 1.21.1, compile-verified 1.21.11, tested-mockbukkit 1.21.11Spigot/CraftBukkit 1.21 are not supported: the modern adapter needs Paper's Adventure API. |
| 26.1 - 26.3.x | Paper and forks | 25 | Supportedmodern adapter | tested-real-server Paper 26.3, compile-verified 26.3Same adapter as 1.21. |
| newer than 26.3.x | Paper | 25+ | Opt-in, untestedmodern adapter | —Only with BloomOptions.allowNewerUntestedVersions(true); Bloom logs a warning. |
| any | Folia | - | Not supported | —Regionised threading needs a dedicated scheduler adapter (planned). |
Updated 2026-10-13. “Supported” means Bloom starts and selects an adapter; the last column lists how that version has actually been verified so far.
Modules
Take only what you use. Each module is a small jar; the adapters are added at runtime only.
How the modules fit together- bloom-bukkit
- Startup, platform detection, adapter loading, scheduler, messages and events.
- bloom-gui
- Inventory menus with layouts, pagination, back navigation and duplication-safe clicks.
- bloom-items
- Item builder, modern material names on every version, persistent custom item ids.
- bloom-config
- YAML with defaults, validation that names the line, migrations and comment-safe saves.
- bloom-storage
- SQLite, MySQL and MariaDB off the main thread, with transactions and migrations.
- bloom-testing
- A tick-by-tick scheduler and controllable clock for plugin unit tests.
- bloom-legacy
- Adapter for Bukkit and Spigot 1.7.10 to 1.8.9 on Java 8.
- bloom-modern
- Adapter for Paper 1.21.1 to 26.3.x with Adventure and MiniMessage.
Errors that tell you what to do
Bloom's exceptions name the file, the line, the server and the fix. A typo in a config value reads like this, not like a stack trace:
config.yml line 7: 'homes.max-homes' expected a whole number but found 'ten'
Missing an adapter? Bloom names the dependency to add. Calling a database on the main thread? It says which executor to use instead.
Built for coding agents too
Every public type and method is published as a machine-readable manifest generated from the source, alongside an llms.txt index and a Claude Code skill that teaches agents to check the installed Bloom version, pick the right adapter and avoid inventing methods.
- llms.txt for documentation discovery
- api-manifest.json with signatures, Javadoc and availability
- AI integration guide for Claude Code, Codex and OpenCode
What Bloom promises
Honest about versions
Bloom refuses to start on a server it has no adapter for and tells you why. Features a version lacks are reported as capabilities you can check, never faked.
The main thread stays free
Database work runs on a bounded pool and fails fast when saturated. Blocking calls on the main thread throw with a message that says how to switch threads.
Nothing left running
Tasks, listeners, menus, boss bars and database pools belong to your plugin and are closed in order when it shuts down.
Items stay in menus
Every click and drag inside a Bloom menu is cancelled, including shift-clicks and number keys, and re-cancelled if another plugin interferes.
Get started
Pick your oldest server
Compile against spigot-api 1.8.8 for one jar that runs everywhere, or against Paper for modern-only plugins.
Add Bloom
Depend on the feature modules you use and add the adapters for the versions you support.
Relocate
Shade Bloom into your plugin under your own package so plugins never conflict.
Enable it
Call Bloom.enable(this) in onEnable and bloom.shutdown() in onDisable.
repositories {
mavenCentral()
// Bloom is not on Maven Central yet. Either unzip bloom-<version>-central-bundle.zip from GitHub Releases and add
// maven(uri("path/to/unzipped-bundle")), or run `./gradlew publishToMavenLocal` in the Bloom repository.
mavenLocal()
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") // spigot-api
// spigot-api 1.8.8 depends on bungeecord-chat 1.8-SNAPSHOT, which only PaperMC's repository still serves.
maven("https://repo.papermc.io/repository/maven-public/")
}
java {
toolchain { languageVersion = JavaLanguageVersion.of(21) }
// The plugin is Java 8 bytecode but bundles Bloom's Java 21 modern adapter, which Bloom only loads on Java 21+.
disableAutoTargetJvm()
}
val bloomVersion = providers.gradleProperty("bloomVersion").getOrElse("0.1.0")
dependencies {
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT") // compile against the oldest API you support
implementation("io.github.crownalonedev.bloom:bloom-gui:$bloomVersion") // menus + items + core
runtimeOnly("io.github.crownalonedev.bloom:bloom-legacy:$bloomVersion") // 1.7.10 - 1.8.9
runtimeOnly("io.github.crownalonedev.bloom:bloom-modern:$bloomVersion") // Paper 1.21.1 - 26.3.x
}
tasks.withType<JavaCompile>().configureEach {
options.release = 8
}
tasks.shadowJar {
archiveClassifier = ""
// Relocate Bloom into your own package so several plugins can embed different Bloom versions.
// The trailing dots matter: they stop the pattern from matching unrelated packages.
relocate("io.github.crownalonedev.bloom.", "com.example.myplugin.libs.bloom.")
}Bloom 0.1.0 is not on Maven Central yet; the installation guide shows how to use the release from GitHub.