# Enterprise

Qualor Enterprise is the same `qualor/server` image with a licence key. Without a key the server
is the community edition: MIT, with no limit on users, projects or lines of code, and at most 3
organisations. With a valid key it becomes the enterprise edition. Nothing is installed or
downloaded, and the key is checked on your server: Qualor never calls home, so it works in
air-gapped networks.

## What a licence adds

| | Community | Enterprise |
|---|---|---|
| Organisations | up to 3 | the number in your licence |
| AI fix suggestions | at most 25 per organisation per day | up to your own budget (**Settings → AI assistant**) |
| Single sign-on (SAML, OIDC), SCIM, roles, audit log, portfolio reports, compliance exports | no | **not yet**: they are being built and will switch on with the same key |

Today a licence changes two things: the organisation limit and the fix suggestion ceiling of the
[AI assistant](https://qualor.dev/docs/ai-assistant.md). The other enterprise features are not available yet, in any
edition.

The enterprise code ships inside the `qualor/server` image, in `/app/enterprise`. It is inert
without a key: the server does not even open the file. There is no separate image to pull and
nothing to switch when you buy a licence.

That code lives in the repository's [`enterprise/`](https://github.com/qualor-dev/qualor/tree/main/enterprise) directory under its own
licence, [`enterprise/LICENSE`](https://github.com/qualor-dev/qualor/blob/main/enterprise/LICENSE). It is source-available, not open source:
you can read it, and copy and change it for development and testing, without a key. Running,
mirroring or redistributing the `qualor/server` image with no key configured needs no
subscription: the enterprise code is not in use then. Using the enterprise features in production
needs a subscription. The licence text is a **draft**: it is still to be reviewed by a lawyer and
may change before the first licence is sold. Everything outside `enterprise/` is MIT.

**Licence keys are not on sale yet.** No release of Qualor accepts a production key so far: the
public key that checks them ships with the first release that sells them. Until then everything on
this page describes how a key will work. To register your interest, use
[qualor.dev/enterprise](https://qualor.dev/enterprise).

## Apply a key

A key is one line of text that starts with `QLK1.`. Give it to the server in one of three ways.
When more than one is present, the server uses the first of this list:

1. **`QUALOR_LICENSE`**, an environment variable holding the key. In the `compose.yml` of
   [Install the server](https://qualor.dev/docs/install-server.md#the-compose-file), add a line under
   `environment:`

   ```yaml
   services:
     server:
       environment:
         QUALOR_LICENSE: ${QUALOR_LICENSE:-}
   ```

   and put the key in your `.env` file (`QUALOR_LICENSE=QLK1.…`). With Helm, keep the key in a
   Secret and pass it with `extraEnv`:

   ```sh
   kubectl -n qualor create secret generic qualor-license --from-literal=license='QLK1.…'
   ```

   ```yaml
   extraEnv:
     - name: QUALOR_LICENSE
       valueFrom:
         secretKeyRef:
           name: qualor-license
           key: license
   ```

2. **`QUALOR_LICENSE_FILE`**, the absolute path of a file holding the key, for example a Docker
   secret:

   ```yaml
   services:
     server:
       environment:
         QUALOR_LICENSE_FILE: /run/secrets/qualor_license
       secrets:
         - qualor_license
   secrets:
     qualor_license:
       file: ./qualor-license.txt
   ```

   The file is read once, at start, and may be at most 16 KiB. The server runs as user 65532, so
   that user must be able to read it. Save it as UTF-8, with or without a byte-order mark. UTF-16
   with a byte-order mark works too: that is what Notepad's "Unicode" and PowerShell 5.1's `>`
   write.

3. **The web UI or the API.** An instance admin opens **Settings → Licence**, pastes the key and
   selects **Save** (or sends it to [`PUT /api/v0/license`](#the-licence-api)). The key is checked
   at once, and a key that is not valid is refused with the reason. A saved key is stored in the
   database, in the instance settings.

Set at most one of the two variables: with both, the server does not start and says
`set QUALOR_LICENSE or QUALOR_LICENSE_FILE, not both`. While a variable is set, **Settings →
Licence** shows the key's status and names the variable, but cannot change or remove the key.

### Restart to apply it

**A new key takes effect at the next start of the server**, whichever way you gave it. After you
save a key in the web UI, the page says "Saved. Restart the server to apply the new key." and shows
**Restart required.** until you restart. Then restart it:

```sh
docker compose up -d server                          # Docker Compose, after a change to .env or compose.yml
docker compose restart server                        # Docker Compose, after saving a key in the web UI
kubectl -n qualor rollout restart statefulset/qualor # Helm with the embedded database (the default)
kubectl -n qualor rollout restart deployment/qualor  # Helm with an external or bundled database
```

At start the server writes one line about the licence to its log, such as:

```json
{"level":30,"component":"licence","licence":"active","edition":"enterprise","source":"environment","expires":"2027-10-01T00:00:00.000Z","plugins":["qualor-enterprise"],"msg":"running as the enterprise edition"}
```

It says the state of the licence (`none`, `active`, `grace`, `expired` or `invalid`), the edition,
where the key came from, when it expires and which enterprise plugins loaded. It never contains the
key, the customer's name or the licence id, so you can ship your logs to another service.

A key that the server rejects never stops it. The server starts as the community edition and logs
`licence key rejected` at level error with a `reason` (see [Troubleshooting](https://qualor.dev/docs/troubleshooting.md#the-licence)).
A typo in a key never takes Qualor down.

A `QUALOR_LICENSE_FILE` that the server cannot read **does** stop the start, because a secret that
is not mounted is a mistake in the deployment. The server exits with one of these messages, and
never prints the file's content:

```text
qualor-server: QUALOR_LICENSE_FILE: cannot read /run/secrets/qualor_license
qualor-server: QUALOR_LICENSE_FILE: /run/secrets/qualor_license is not a file
qualor-server: QUALOR_LICENSE_FILE: /run/secrets/qualor_license is larger than 16 KiB
qualor-server: QUALOR_LICENSE_FILE: /run/secrets/qualor_license is not a text file in UTF-8 (or UTF-16 with a byte-order mark); save it as UTF-8
```

### Pasting a key

A key copied from an e-mail is often broken over several lines. That is fine: the server removes
spaces and line breaks before it checks a key, in every one of the three ways. The web UI also
removes invisible characters that some mail and chat programs add (non-breaking spaces, zero-width
spaces, byte-order marks). After you save a key, the page never shows it again.

## Check the licence

**Settings → Licence** (instance admins only) shows:

- the edition and the state in words, such as "active until 1 October 2027", or the reason a key
  was rejected;
- whom the licence is for, the licence id, and when it was issued and expires;
- how many organisations you use of how many are allowed, and how many are read-only;
- the active enterprise features and the enterprise plugins, with any load error;
- where the key comes from: `QUALOR_LICENSE`, `QUALOR_LICENSE_FILE`, or saved in Qualor.

Unless a variable sets the key, the page also has a field to paste a key with **Save** and, once a
key is saved in Qualor, a **Remove** button.

Every signed-in user can see the edition, the active features and the organisation limit in
`GET /api/v0/system/info`. Users who are not instance admins see nothing else about the licence.

## When a licence expires

1. From 30 days before the expiry date, instance admins see a notice at the top of every page: "The
   Qualor licence expires on … Renew it to keep the enterprise features." It links to the licence
   page.
2. On the expiry date a **14-day grace period** starts. Everything keeps working, and the notice
   says when the enterprise features stop.
3. When the grace period ends, the server runs as the community edition, without a restart:
   - the AI assistant's fix suggestions are limited to 25 per organisation per day again. A higher
     fix budget you saved stays saved and applies again with a renewed key; until then you can keep
     it or lower it in **Settings → AI assistant**, but not raise it;
   - **organisations beyond the third become read-only** (see below);
   - **nothing is deleted.** Projects, issues, history and settings all stay.
4. Apply a renewed key and restart: everything works as before, with all its data.

Removing a key, or replacing it with one that allows fewer organisations, has the same effect at the
next start.

## Read-only organisations

When a server has more organisations than its edition allows, the **oldest** organisations stay
as they are, up to the limit (3 in the community edition), and the newer ones become **read-only**.
This only happens after a licence lapses, is removed, or is replaced by a smaller one: you can never
create an organisation beyond the limit (`ORG_LIMIT_REACHED`).

In a read-only organisation:

- **Everything can still be read.** The web UI, the REST API, exports and downloads work as before.
  Everyone who can see the organisation sees the marks: the organisation switcher says "(read-only)" after its name, and
  each of its project pages says why changes fail. Instance admins also see a notice at the top of
  every page with the number of read-only organisations. A refused change in the web UI says how
  many organisations the licence allows.
- **Every change is refused** with HTTP 409 and the code `ORG_READ_ONLY`: projects, tokens,
  branches, issue statuses, quality gates, profiles, webhooks, GitLab/GitHub connections, members
  and AI assistant requests. That includes a few requests that only look at data but are sent as
  `POST`: the dry run of `qualor import sonarqube` (for issue statuses) and the connection test in
  **Settings → GitLab** and **GitHub**.
- **New analyses are refused.** A CI job that uploads to one of its projects fails with exit code 4
  and says why, instead of losing the analysis silently:

  ```text
  error: the server rejected the upload (409 ORG_READ_ONLY: This organization is read-only: the licence allows 3 organizations. Renew the licence, or ask for a key with a higher organization limit.)
  ```

- Work that was already queued finishes, and the normal housekeeping still runs: old uploaded
  reports, closed issues and webhook deliveries are cleaned up on the usual
  [retention](https://qualor.dev/docs/install-server.md#retention) schedule.
- Instance-level settings are not affected. Users, personal tokens, sign-in and the licence itself
  work as usual, and an instance admin can still change an organisation's AI assistant settings in
  **Settings → AI assistant**.

The API's 409 answer carries the limit as a number, `organizationLimit`, next to `code`, so a
script can report it.

`GET /api/v0/organizations` tells you which organisations are read-only: each item has a
`readOnly` field (`true` or `false`).

A key that allows enough organisations makes them all writable again at the next start, with all
their data.

## The licence API

Instance admins can manage the key over the REST API, with a session or a personal token with the
Admin scope. The key text is never returned by any of these endpoints.

```sh
export QUALOR_URL=https://qualor.example.com
H="Authorization: Bearer $QUALOR_ADMIN_TOKEN"

curl -fsS -H "$H" "$QUALOR_URL/api/v0/license"                     # the status
curl -fsS -X PUT -H "$H" -H 'Content-Type: application/json' \
  -d "{\"key\":\"$(tr -d '\r\n' < qualor-license.txt)\"}" "$QUALOR_URL/api/v0/license"  # save a key
curl -fsS -X DELETE -H "$H" "$QUALOR_URL/api/v0/license"           # remove the saved key
```

| Endpoint | What it does |
|---|---|
| `GET /api/v0/license` | the edition, the state (`none`, `invalid`, `active`, `grace` or `expired`), the reason a key was rejected, where the key comes from, the licence's customer, dates, organisation limit and features, the organisation count with the limit and the number of read-only organisations, the active features and the plugins, and `restartRequired` |
| `PUT /api/v0/license` `{"key": "QLK1.…"}` | checks the key and saves it. It answers with the status, with `restartRequired: true` until the next start |
| `DELETE /api/v0/license` | removes the saved key. The server runs as the community edition from the next start |

| Status and code | Meaning |
|---|---|
| 422 `LICENSE_INVALID` | the key was refused. The `reason` field gives the reason as a code: `malformed` (not a Qualor licence key: check that it was copied completely), `unknown-key` (signed with a key this version of Qualor does not accept), `bad-signature` (changed: the signature does not match), `bad-payload` (contents not valid), `revoked`, or `not-yet-valid`. `errors[0].message` says the same in English |
| 422 `LICENSE_EXPIRED` | the key is past its grace period: ask for a renewed one |
| 409 `LICENSE_MANAGED_BY_ENVIRONMENT` | the key comes from `QUALOR_LICENSE` or `QUALOR_LICENSE_FILE`. Change the variable and restart instead |
| 413 `BODY_TOO_LARGE` | the request body is larger than 17 KiB. A key is far smaller: check what you sent |
| 403 | you are not an instance admin, or the token lacks the Admin scope |

## Questions

- **Is the key tied to a server?** No. It works on any server of yours, including a test instance
  and a standby.
- **Does the key send anything anywhere?** No. It is checked offline, with a public key built into
  Qualor.
- **Several replicas?** Each replica reads the key when it starts. A key saved in the web UI is
  stored in the database, so all replicas use it after their next start.
- **What if the server's clock is wrong?** The expiry uses the server's clock. A key issued "in the
  future" by more than a day is refused as not valid yet: check the clock.
- **A key that is not valid yet?** The server checks the key once, when it starts. If it started
  more than a day before the key's issue date, it keeps running as the community edition: restart it after the
  issue date. Saving such a key in the web UI is refused the same way until then.