> 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/how-it-works.md).

# How It Works

Obsidian License sits between your plugin and your customers. Your plugin asks the platform whether a license is valid; the platform answers in a way that cannot be forged, and logs everything.

## License check

Your plugin ships as an ordinary jar and calls the server on startup.

```
        ┌────────────────────────┐     POST /api/v1/validate     ┌──────────────────────┐
        │  Customer Paper server  │  ──────────────────────────▶  │   License Server     │
        │  your plugin (normal)   │   key + productId + nonce     │  checks key, seats,  │
        │                         │  ◀──────────────────────────  │  expiry, revocation  │
        │  enables, or disables   │   signed yes + tier + limits  │  logs every attempt  │
        └────────────────────────┘                               └──────────────────────┘
```

1. The plugin generates a random nonce and POSTs the key, product id and a machine id, signed with `X-Signature` (HMAC-SHA256 keyed by the license key).
2. The server checks the key: exists, right product, not revoked, not expired, a seat is free for this machine, not throttled, IP not banned.
3. It replies with a yes plus tier and entitlements, Ed25519-signed over a message containing your nonce.
4. The plugin verifies the signature against the pinned public key, then enables itself. Any failure and it disables itself.

Because the nonce is chosen by your plugin and the reply is signed, a recorded response cannot be replayed and a fake server cannot answer "valid".

See [license-check.md](/products/obsidian-license/license-check.md).

## Protected plugin

The compiled plugin never reaches the customer's disk. They install a thin loader.

```
        ┌────────────────────────┐     POST /api/v1/activate     ┌──────────────────────┐
        │  Customer Paper server  │  ──────────────────────────▶  │   License Server     │
        │  loader-plugin (thin)   │   key + machine fingerprint   │  encrypts jars at    │
        │                         │  ◀──────────────────────────  │  rest, logs access   │
        │  in-memory classloader  │   enc(payload) + key + sig    │                      │
        │  runs the real plugin   │                               │                      │
        └────────────────────────┘                               └──────────────────────┘
```

Same checks, but the response also carries your AES-256-GCM encrypted jar and its key, signed as `nonce.version.payload.key.ent`. The loader verifies, decrypts in memory, and runs the plugin through a custom class loader. Nothing touches disk.

See [protected-plugins.md](/products/obsidian-license/protected-plugins.md).

## Three hosts, one implementation

Both modes run on Paper, on Velocity, and inside a plain Java application. The activation, signature verification, decryption and in-memory class loading are the same code everywhere; only the lifecycle around them differs (a Bukkit `onEnable`, a Velocity `ProxyInitializeEvent`, or your own `main`). A protected module written against the neutral `ModuleContext` runs on all three unchanged.

## The lifecycle around both

1. **Create a product.** Upload a build if you want the platform to serve it, either encrypted to loaders or as a [direct download](/products/obsidian-license/downloads.md).
2. **Issue keys**, manually, in bulk, or automatically from a [marketplace purchase](/products/obsidian-license/marketplace-automation.md).
3. **Servers check in.** Every attempt, allowed or denied, is logged with IP, OS and machine id, and streamed live to your dashboard.
4. **Stay in control.** Revoke a key, let it expire, change a tier, move a customer to the beta channel, free a seat, ban an abusive IP. The change takes effect on the affected servers on their next check.

## Fail closed

Both modes fail closed. If the config is wrong or the license server is unreachable, the plugin does not run. That is deliberate, there is no offline grace period by default. When a check is denied, the customer gets a plain sentence explaining what to do and a link to the self-service portal rather than a stack trace.

## Seats and machine binding

A license runs on a set number of machines. Both modes count seats the same way, keyed on a stable per-server machine id. When a customer migrates hardware they free the old seat themselves at [license.ohalee.com/portal](https://license.ohalee.com/portal) with their key, which removes the most common support ticket a licensed plugin generates.
