# Licensing

> Offline-verified EdDSA license keys + cloud-issued entitlement snapshots that feed plugin-billing; pricing decided server-side, never baked into a published version.



---

<!-- source: en/plugins/licensing.md -->
## Licensing

_Offline-verified EdDSA license keys + cloud-issued entitlement snapshots that feed plugin-billing; pricing decided server-side, never baked into a published version._

`@voltro/plugin-licensing` lets a **cloud** decide a customer's plan entitlements and pricing, and enforces them in a **running app** — without baking anything into an immutable published version. The license is a compact **EdDSA (Ed25519)** token, verified **offline** with a public key the app embeds. It's asymmetric on purpose: the signing secret never leaves the cloud, so a customer cannot forge a license.

The plugin does not enforce quotas itself — it feeds [`plugin-billing`](/docs/plugins/billing), which meters and enforces them via a new `resolveEntitlementLimit` seam.

## Wiring

```ts
// app.config.ts
import { licensingPlugin, entitlementResolver } from '@voltro/plugin-licensing'
import { billingPlugin } from '@voltro/plugin-billing'

export default {
  type: 'api' as const,
  name: 'api',
  plugins: [
    licensingPlugin(),                          // reads VOLTRO_LICENSE_* env
    billingPlugin({
      plans: { free: { entitlements: { projects: 1 } } },  // static fallback
      resolveEntitlementLimit: entitlementResolver,        // license overrides per-tenant
    }),
  ],
}
```

With a valid `pro` license granting `{ projects: 3 }`, `requireEntitlement(ctx, 'projects', 1)` allows three and then fails `EntitlementExceeded`. With no license, the static `free` plan (`projects: 1`) applies — the resolver returns `null` and billing falls back to its own registry.

## Keys

Generate a signing keypair:

```sh
voltro secret keypair
# VOLTRO_LICENSE_SIGNING_KEY  (Ed25519 private, cloud-only — signs licenses, NEVER ship)
# VOLTRO_LICENSE_PUBLIC_KEY   (Ed25519 public — ship to apps for offline verify)
```

Keep the private key in the cloud; the public key is safe to embed. The app reads `VOLTRO_LICENSE_PUBLIC_KEY` + `VOLTRO_LICENSE_KEY` (or the `publicKey` / `licenseKey` options).

## Options

| Option | Env fallback | Meaning |
|---|---|---|
| `publicKey` | `VOLTRO_LICENSE_PUBLIC_KEY` | Ed25519 public key (SPKI/PEM). |
| `licenseKey` | `VOLTRO_LICENSE_KEY` | The signed license token, verified at boot. |
| `snapshotUrl` | — | URL to periodically re-fetch a fresh license (entitlement sync). |
| `refreshMs` | — | Refresh cadence for `snapshotUrl` (default 15 min, cluster-coordinated). |
| `graceSeconds` | — | Seconds an expired license is still honored (default 0). |

## API

- **`entitlementResolver(tenantId, key)`** — pass to `billingPlugin({ resolveEntitlementLimit })`. Returns the license's limit for the pair, or `null` to fall back to the static plan registry.
- **`LicenseService`** — `yield* LicenseService` in a handler to read `limitFor` / `hasFeature` / `allowsPlugin` / `edition` / `snapshot` directly.
- **`licenseAllowsPlugin(name)` / `requireLicensedPlugin(name)`** — the plugin gate ("installed ≠ billable"). A license carries a `plugins: string[]` grant; `requireLicensedPlugin` fails `PluginNotLicensed` when a plugin's production use isn't licensed. Metered plugin pricing uses entitlement keys (`plugin:<name>:<meter>`); plan-gated plugins use the grant. Pricing is decided server-side and travels in the signed license — no `plugin_definitions` table needed in the app.
- **`verifyLicense(token, publicKeyPem)`** — offline EdDSA verify → `LicenseClaims`.
- **`@voltro/plugin-licensing/sign` → `signLicense(input, privateKeyPem)`** — server-only signer for the cloud/CLI to mint licenses.

## Notes

- **Server-only** (imports `jose`) — never import from a browser-loaded descriptor.
- **Fail-closed**: an invalid/expired token grants nothing; billing falls back to the static plan. The last-good snapshot is honored within `graceSeconds`.
- Cloud-side issuance (a license endpoint + `voltro cloud register`) ships in a later milestone; today licenses are provided via env / config.
