> 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/for-developers.md).

# For Developers

To protect a plugin you implement the `ProtectedModule` contract from the `protected-api` module. Your plugin is **not** loaded by Bukkit's plugin manager — the thin loader downloads the encrypted jar, decrypts it in memory, finds the class named by the `Obsidian-Module` manifest attribute, instantiates it via its public no-arg constructor, and drives its lifecycle.

## The contract

```java
public interface ProtectedModule {

    /**
     * Called once after the license has been validated and the module loaded
     * into memory.
     *
     * @param context bridge to the host JavaPlugin (events, commands, logger, config)
     */
    void onEnable(PluginContext context);

    /**
     * Called when the host plugin is disabled (server stop / reload). Clean up
     * here; registered listeners/commands are also torn down automatically.
     */
    void onDisable();
}
```

{% hint style="warning" %}
Don't keep static references that would survive a reload — the module is loaded by a custom in-memory class loader that gets torn down and recreated.
{% endhint %}

## PluginContext

The loader hands your module a `PluginContext` so it can integrate with the running server without being a Bukkit plugin itself.

```java
public interface PluginContext {
    JavaPlugin plugin();                 // the host loader (schedulers, config folder, ...)
    Logger logger();                     // namespaced to the module
    String licenseKey();                 // the activating server's license key

    String tier();                       // free-form SKU label, or null
    Map<String, Object> entitlements();  // per-license flags/limits, never null

    // convenience readers over entitlements()
    default boolean hasEntitlement(String key);
    default long entitlementLong(String key, long fallback);

    void registerListener(Listener listener);                 // auto-unregistered on disable
    void registerCommand(String name, String description, CommandExecutor executor);
    void registerCommand(String name, String description, CommandExecutor executor, TabCompleter completer);
}
```

`registerCommand` registers a command that does **not** exist in any `plugin.yml` via Bukkit's `CommandMap`, and unregisters it on disable.

### Tier & entitlements

`tier()` and `entitlements()` are delivered **inside the signed activation envelope**, so they're tamper-proof. This is how one product sells Basic vs Premium with no separate builds — the module reads them at runtime:

```java
@Override
public void onEnable(PluginContext ctx) {
    boolean premium = "premium".equals(ctx.tier());
    long maxHomes = ctx.entitlementLong("max_homes", 3);
    if (ctx.hasEntitlement("pvp_toggle")) {
        // enable a gated feature
    }
    ctx.registerListener(new MyListener());
    ctx.registerCommand("mycmd", "Does the thing", new MyCommand());
}
```

## Classloader boundary

`PluginContext` and `ProtectedModule` live in `protected-api`, which is bundled into the **loader** and owned by the loader's classloader. Protected modules reference `protected-api` as `compileOnly` and resolve it at runtime via parent delegation — so casts across the loader/module boundary stay valid. Don't shade `protected-api` into your module jar.

## Packaging and uploading

Build your plugin the way you normally would (a standard Paper jar) — just make it implement `ProtectedModule` and set the `Obsidian-Module` manifest attribute to your entry class. Then **upload the jar** to your product in the dashboard; the platform encrypts it and serves it to loaders. You do **not** build the loader yourself — download it from the dashboard and ship it to customers.

## Automating uploads from CI

Generate a personal **API key** in the dashboard and use it to upload a freshly built jar as a new product version from your CI pipeline. New uploads are retained as versions, so you can put trusted customers on a **beta** channel and promote or roll back instantly.
