For the complete documentation index, see llms.txt. This page is also available as Markdown.

๐Ÿ“ฆShip Your Own Loader

Every protected plugin ships its own loader, named after itself. Here is why, and the ten lines it takes.

If you sell a protected plugin, build a loader named after your plugin and ship that. Do not ship the generic one.

Why

A loader hosts exactly one protected module. It has one config file, one product id, one license key, and one module in memory.

Bukkit refuses to enable two plugins with the same name, and Velocity refuses two plugins with the same id. So if every seller shipped the stock ObsidianLoader, a customer who bought protected plugins from two different sellers could install exactly one of them. The second would fail to load, and the two would fight over the same plugins/ObsidianLoader/config.yml anyway.

Name the loader after your plugin and the problem disappears. Your customers install MyPlugin, someone else's install TheirPlugin, and both run happily side by side.

The generic loader from the dashboard is still useful: for testing, and for a customer who only ever runs one protected plugin. It is just not what you distribute.

Get the code

Everything the loader is made of is public:

Use velocity instead of paper for a proxy plugin. You can also just copy the sources into your project if you would rather not add a dependency.

Selling for 1.8? Use v1.2.1 or newer. Earlier releases reached the command map through Server#getCommandMap(), which Paper only added in 1.19.4, so registering a module's commands threw NoSuchMethodError on older servers. The loader now resolves it reflectively off the concrete server class, which works on every generation.

Whatever version you use, remember that the paper module compiles against a current Paper API. If your loader itself calls a Bukkit method that does not exist on your oldest target, that is a runtime failure on the customer's server, not a compile error on yours. Compiling your loader against your oldest supported API as a build step is the cheapest way to catch it.

The loader, in full

plugin.yml:

config.yml, which is all your customer ever edits:

Hardcode the server URL, product id and public key rather than putting them in config.yml. The only thing a customer should have to paste is their license key, and a pinned public key they cannot edit is a public key nobody can talk them into changing.

Shade the dependency so the loader carries its own classes:

Velocity

The same shape, with VelocityLoaderSupport and your own plugin id:

Full template: MyProxyLoader.java.

Plain Java

There is no plugin to name, so there is no collision to avoid: use ObsidianLoader directly from your own main. See StandaloneHost.java.

What you ship

Two jars, from two builds:

Jar
Built from
Goes to

MyPlugin.jar (the loader)

Your loader project

Your customers

my-plugin-1.0.0.jar (the module)

Your real plugin

Uploaded to the dashboard, never shipped

The module jar is the one that stays secret. The loader is public by nature: it runs on customer machines and can be decompiled by anyone, which is why none of its security depends on being hidden. It pins a public key, and every real check happens on the server.

Keeping it up to date

Bump the dependency when the platform's signed envelope changes, then rebuild and redistribute your loader. Older loaders fail closed with "response signature invalid" rather than doing anything unsafe, but your customers will notice. Release notes call out any change that needs this.

Last updated