---
title: "Licensing - Pilot Docs"
description: "License management, validation, and feature gating"
url: "https://docs.calinora.io/reference/licensing/"
---

# Licensing

Pilot operates in two modes:

- **Monitoring mode** (free) - cluster overview, health monitoring, activity scoring, consumer groups, Prometheus metrics, read-only API
- **Management mode** (licensed) - partition redistribution, broker maintenance, topic config updates, quota management, ACL management, reassignment execution

## License Format

Licenses are offline JWT tokens signed with Ed25519. No internet connection is required for validation - only for automatic license fetching.

Claims used: `sub` (customer), `lic` (license ID), `iat`, `nbf`, `exp`, plus optional `notes`. Clock skew leeway is 300 seconds.

## Configuration

| Variable | Default | Description |
| - | - | - |
| `LICENSE_STRING` | `""` | JWT license token |
| `LICENSE_FETCH_SUBSCRIPTION_ID` | `""` | Subscription ID for auto-fetch |
| `LICENSE_FETCH_TOKEN` | `""` | Per-customer fetch token |
| `LICENSE_FETCH_URL` | `https://license.calinora.io/api/license/fetch` | License fetch endpoint |
| `LICENSE_FETCH_INTERVAL` | `1h` | Auto-fetch interval |

### Automatic Fetching

When `LICENSE_FETCH_SUBSCRIPTION_ID` and `LICENSE_FETCH_TOKEN` are set, Pilot fetches a fresh license automatically at the configured interval. This is the recommended approach for subscription and enterprise customers.

### Manual Configuration

Set `LICENSE_STRING` directly with the JWT token:

```yaml
# docker-compose.yml
environment:
  LICENSE_STRING: "${LICENSE_STRING:-}"
```

### Manual Fetch

For environments without outbound internet, fetch the license externally and provide it via `LICENSE_STRING`:

```bash
curl -X POST "https://license.calinora.io/api/license/fetch" \
  -H "Authorization: Bearer <FETCH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"subscription_id":"sub_123"}'
```

## Network Requirements

If using automatic license fetching, outbound HTTPS (port 443) access to `license.calinora.io` is required.

## API

### Check License Status

```
GET /api/v1/license
```

Response:

```json
{
  "success": true,
  "data": {
    "state": "licensed",
    "expires_at": "2026-01-15T00:00:00Z",
    "lic": "abc123-def456",
    "sub": "CustomerName"
  }
}
```

Possible `state` values: `licensed`, `trial`, `expired`, `invalid`.

Quick test:

```bash
curl -s http://localhost:8080/api/v1/license | jq '.'
```

## License Enforcement

Mutating endpoints are protected by license middleware:

- **Licensed endpoints** return `403 Forbidden` with a structured JSON error when the license is invalid or expired
- **Free endpoints** (GET requests, proposal generation, what-if simulation) remain accessible regardless of license state

### On Expiry

An expired or missing license never affects the cluster. Pilot falls back to monitoring mode: dashboards, health checks, proposals, and Prometheus metrics keep running; self-healing loops skip with the reason `license_invalid` and apply nothing; reassignments already submitted to Kafka complete normally. With automatic fetching, a renewed license takes effect at the next fetch without a restart; with `LICENSE_STRING`, update the value and restart Pilot.

See [Features](https://docs.calinora.io/features/) for which operations require a license.

## Prometheus Metrics

| Metric | Description |
| - | - |
| `pilot_license_valid` | `1` if licensed, `0` if expired/invalid |
| `pilot_license_expiry_timestamp_seconds` | Unix timestamp of license expiry |
| `pilot_license_fetch_total` | Auto-fetch attempts by result |

Alert when license expires within 7 days:

```promql
pilot_license_expiry_timestamp_seconds - time() < 7 * 24 * 3600
```
