# Licence

> Install the offline licence key, what it unlocks, and what happens when it expires.

Source: https://critlab.app/docs/self-hosting/license

A self-hosted install runs without a licence key. A key from Supermode unlocks the Team or Enterprise feature set. It is checked offline, never calls home, and never locks anyone out of their data.

## What works without a key

Uploading and publishing, share links in every access mode except members-only, commenting, voice notes, reactions, presence and follow mode, folders, the `crit` CLI and the MCP round-trip with your coding agent. Plan **limits** (how many prototypes, how large a package) come from each workspace's plan, which the operator sets:

```bash
pnpm --filter @critlab/db set-plan <workspace-slug> pro
```

## What a key unlocks

| Feature | Team edition | Enterprise edition |
| --- | --- | --- |
| Custom share domains | ✓ | ✓ |
| Integrations (Slack-compatible webhooks; Linear and Jira when enabled) | ✓ | ✓ |
| Members-only prototype access | ✓ | ✓ |
| Audit log export | ✓ | ✓ |
| SAML single sign-on and verified email domains | | ✓ |
| SCIM directory sync | | ✓ |
| Bring your own bucket, region pinning | | ✓ |

A key can also name its features explicitly, for example Team plus SSO. The key carries a seat count and an expiry date, and may be bound to one deployment by name.

On a self-hosted install, features come from the key, not from the plan: `set-plan <slug> enterprise` raises limits and grants no features. A refused feature answers `feature_not_licensed` (HTTP 402) with a message naming the feature.

## The key

A key is one line: `critlab-lic.<payload>.<signature>`. The payload names the licensee, edition, seats, features, issue and expiry dates and, optionally, an instance id. The signature is Ed25519, verified with Node's own crypto against a public key compiled into the build — no network access, which is what makes it work air-gapped.

## Install a key

**Helm, from a Secret you manage (recommended):**

```bash
kubectl -n critlab create secret generic critlab-license \
  --from-literal=LICENSE_KEY='critlab-lic.…'
helm upgrade critlab charts/critlab -n critlab --reuse-values \
  --set license.existingSecret=critlab-license
```

`license.existingSecretKey` renames the key inside the Secret if `LICENSE_KEY` does not suit you. `license.key` puts the token in the chart-managed Secret instead.

**From a file:** set `LICENSE_KEY_FILE=/etc/critlab/license.key` (for example a mounted Secret). `LICENSE_KEY` wins if both are set.

**From the app:** a platform admin can paste a key in **Settings → License**. The key is verified first, then stored at `LICENSE_KEY_FILE` if that path is writable; otherwise the page tells you which variable or Secret to set. A key that has expired past its grace period is refused, because installing it would switch the licensed features off for everyone.

A license decides what every workspace on the instance may do, so Settings → License belongs to the operator, not to a workspace owner (everyone who signs up owns a personal workspace). One step first: make your operator account a platform admin, from a checkout with `DATABASE_URL` pointing at your database, as with `set-plan`:

```bash
pnpm --filter @critlab/web admin:grant --email you@example.com
```

Sign in as that account and **Settings → License** appears. Anyone else gets a 404 for the page and for `/api/license`. `crit license` and the `critlab_get_license` / `critlab_set_license` tools no longer work, because a platform-admin setting is never reachable with an API key.

**Bound keys:** if your key was issued for one deployment, set `INSTANCE_ID` on web to the name it was issued for (through `web.extraEnv` with Helm). A bound key is refused anywhere else.

web re-reads the key at most once a minute, so installing or renewing one needs no restart. The licence lives in the deployment, never in the database: restoring an old database backup does not restore an old licence.

## Check it

**Settings → License** (platform admin only) and `GET /api/license` called with that admin's browser session report the licensee, edition, seats used against seats allowed, expiry, the exact feature list and, if something is wrong, the verifier's reason — a signature that does not match this build, a key bound to another instance, an expired key.

## Expiry and grace

| State | When | Effect |
| --- | --- | --- |
| Valid | Before the expiry date | Everything the key grants works. |
| Grace | Up to 14 days after expiry | Everything still works; owners see a renewal banner on the dashboard. |
| Expired | After the grace period | The licensed features go read-only. Prototypes, versions, comments, transcripts and sign-in are never affected. |

There is no revocation list — offline verification cannot have one — so keys are issued with sensible expiry dates and renewed.

> **Getting a key**
>
> Keys are issued by Supermode. Tell us the seat count and, if you want the key bound to one deployment, the `INSTANCE_ID` you will set. Evaluation keys are available on request.

> **Open questions**
>
> - **Free-forever self-hosting.** A proposal (D-090) would make the self-hosted comments use case unlimited and free without a key, and move some features between the free and paid sides. It is not decided; this page describes what the code does today.
> - **The staging snippet.** It has its own licence feature (`staging_snippet`) that neither edition grants by default, so a key must name it explicitly. Whether that is intended is unconfirmed.
