> For the complete documentation index, see [llms.txt](https://docs.ohalee.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ohalee.com/products/obsidian-license/your-own-loader.md).

# 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](/products/obsidian-license/protected-plugins.md), 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:

{% embed url="<https://github.com/ohAleee/obsidianlicense-api>" %}

```kotlin
repositories {
    mavenCentral()
    maven("https://jitpack.io")
    maven("https://repo.papermc.io/repository/maven-public/")
}

dependencies {
    compileOnly("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT")
    implementation("com.github.ohAleee.obsidianlicense-api:paper:v1.2.1")
}
```

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.

{% hint style="info" %}
**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.
{% endhint %}

## The loader, in full

```java
package com.example.myplugin;

import com.obsidian.license.core.ObsidianLoader;
import com.obsidian.license.loader.PaperLoader;
import org.bukkit.plugin.java.JavaPlugin;

public final class MyPluginLoader extends JavaPlugin {

    // You know these three, so hardcode them.
    private static final String SERVER_URL = "https://license.ohalee.com";
    private static final String PRODUCT_ID = "my-plugin";
    private static final String PUBLIC_KEY = "MCowBQYDK2VwAyEA...";   // Dashboard, Server key

    private ObsidianLoader loader;

    @Override
    public void onEnable() {
        saveDefaultConfig();
        try {
            loader = PaperLoader.builder(this)
                    .serverUrl(SERVER_URL)
                    .productId(PRODUCT_ID)
                    .serverPublicKey(PUBLIC_KEY)
                    .licenseKey(getConfig().getString("licenseKey", ""))
                    // .variant("game")   only if your product ships several jars
                    .build();
            loader.start();
        } catch (Exception e) {
            // Fail closed. The message is written for the customer, so print it as-is.
            getLogger().severe("License check failed: " + e.getMessage());
            getServer().getPluginManager().disablePlugin(this);
        }
    }

    @Override
    public void onDisable() {
        if (loader != null) loader.stop();
    }
}
```

`plugin.yml`:

```yaml
name: MyPlugin          # your name, not ObsidianLoader
version: '1.0.0'
main: com.example.myplugin.MyPluginLoader
api-version: '1.21'
load: STARTUP
```

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

```yaml
licenseKey: "OBS-XXXX-XXXX-XXXX-XXXX"
```

{% hint style="info" %}
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.
{% endhint %}

Shade the dependency so the loader carries its own classes:

```kotlin
tasks.shadowJar {
    relocate("com.google.gson", "com.example.myplugin.libs.gson")
}
```

## Velocity

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

```java
@Plugin(id = "myplugin", name = "MyPlugin", version = "1.0.0")
public final class MyProxyLoader {

    @Subscribe
    public void onInit(ProxyInitializeEvent event) {
        try {
            String key = VelocityLoaderSupport.readConfig(dataFolder).getOrDefault("licenseKey", "");
            loader = VelocityLoaderSupport.builder(this, proxy, dataFolder)
                    .serverUrl(SERVER_URL).productId(PRODUCT_ID).serverPublicKey(PUBLIC_KEY)
                    .licenseKey(key)
                    .build();
            loader.start();
        } catch (Exception e) {
            // Velocity cannot disable a plugin at runtime, so register nothing and say why.
            logger.error("License check failed, this plugin will NOT run: {}", e.getMessage());
        }
    }
}
```

Full template: [MyProxyLoader.java](https://github.com/ohAleee/obsidianlicense-api/blob/main/examples/MyProxyLoader.java).

## Plain Java

There is no plugin to name, so there is no collision to avoid: use [`ObsidianLoader`](https://github.com/ohAleee/obsidianlicense-api/blob/main/core/src/main/java/com/obsidian/license/core/ObsidianLoader.java) directly from your own `main`. See [StandaloneHost.java](https://github.com/ohAleee/obsidianlicense-api/blob/main/examples/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.
