ADR 0009: The licence lifecycle, and what a freeze may stop

Date: 2026-09-11 Status: accepted

Context

ADR 0004 settled how a licence is verified: an ed25519 payload checked locally, with no call home, a grace period on expiry, and the rule that nothing is ever deleted for licence state. What it left vague is the part a customer actually experiences — when they are told, what changes at each point, and what "after grace, new things are refused" means in practice.

Vague turned out to mean wrong. Before this ADR, three things were true at once and none of them were intended:

The commercial context that shapes all of this: the buyer is a bank or a government body, renewal is a procurement process measured in weeks, and the person who signs it is not the person running the product. A warning that arrives a week before expiry is a warning that arrives too late to act on.

Decision

The schedule is fixed and stated. Notices at 30, 14, 7, 3 and 1 days before expiry, on the expiry day, weekly through grace, and once when grace ends. A trial uses 7, 3 and 1 instead: a 14-day evaluation on the ordinary schedule would open with a "30 days left" notice that was never true.

One notice per milestone, and only the most urgent one. If the API was stopped for three weeks and comes back with four days left, the customer gets one accurate mail, not the 30-, 14- and 7-day notices arriving together — which reads as a fault in the product, and three quarters of it is false. The last milestone sent is persisted with the licence ID, so a restart is silent and a renewal starts the sequence again. Keyed on the milestone alone, a customer who renewed with ten days left would have "already sent" everything and heard nothing at all the following year.

Five channels, because in a regulated site at least one is unavailable. Email through the customer's own relay; a Prometheus gauge nazeel_licence_seconds_until_expiry, negative once expired; a Kubernetes Event on the licence ConfigMap; a banner on every dashboard page; one line on the CLI's stderr per run. The last four need nothing configured. An air-gapped site with no mail relay is still told, four different ways.

The CLI learns the state from a response header on requests it was already making, rather than fetching /me before every command. A warning is not worth doubling the request count, and a warning that depends on an extra round-trip is least reliable exactly when the API is struggling.

The freeze stops new work and nothing else. After grace, these are refused: creating an environment, extending its TTL, syncing it, building from source, opening a terminal, per-PR environments from webhooks, the AI assist, the executive reports, and inviting new people.

These are never refused, at any licence state: signing in, reading the dashboard, opening a preview URL, waking a sleeping environment, reading logs, reading or exporting the audit log, and downloading the usage report. Environments are never deleted, scaled to zero, stopped or otherwise touched because of licence state.

The dividing line is: does this cost the customer compute they have not paid for, or is it a feature on the price list? Waking an environment a developer created last week is neither. Reading the audit log is neither, and an auditor being told the evidence is unavailable for commercial reasons would be worse than not selling the product at all.

Enforced in the API and in the admission webhook. Anyone with kubectl can write an Environment directly, so a check that lives only in the API is not a check. The webhook refuses creation at expired and invalid, and allows updates to environments that already exist — taking running work away over a purchase order is the one thing this product must never do.

A renewed key lifts the freeze on the next request. No restart, no cache to expire. The state is derived from the stored key on each check, which is what makes that free.

missing and invalid are different. No key at all is the free Community tier and works (ADR 0004). An invalid key — bad signature, or a development key in a release build — is refused, because somebody installed something wrong and silently downgrading them to the free tier would hide it.

Consequences

Addendum 2026-09-12: where evidence lives

This ADR settled what a licence may stop. A related question came up while writing the disaster recovery runbook and is settled here, because it is the same kind of decision: what happens to the things a customer may be asked to produce.

Evidence may leave the cluster, to storage the customer owns, and only if they ask. The audit archive, the terminal recording and the database backup are the three files a regulator or an auditor asks for. Keeping them only on the API's volume means losing the cluster loses them, and it means the answer to "who can alter this evidence" is "whoever administers Nazeel" — which is a weaker answer than a bank wants about its own audit trail.

So there is an optional S3-compatible evidence store: an endpoint an administrator enters, off until they do, treated exactly like the SMTP relay and the SIEM. It does not weaken the no-outbound-calls rule, it uses it: the product talks only to endpoints the customer configured, and this is one more of those.

Object lock is the point, not a nicety. A bucket with object lock holds an archive that nobody can alter or delete until its retention expires — not the customer's administrator, and not Nazeel, which has no path to it beyond a PutObject. That is a stronger property than append-only triggers in a database the product itself owns, and it is what turns "we keep an audit log" into "we keep an audit log we could not rewrite if we wanted to". Nazeel detects and reports whether the bucket has it, and never tries to set it: turning on a WORM control on a customer's bucket is their decision, made once, deliberately.

Nazeel never deletes an archive or a backup it has uploaded. Their lifecycle is the bucket's, which is the whole reason for putting them somewhere the product cannot reach. Recordings are different and do expire there, because the team's retention policy is about the recording itself rather than about which disk it happens to be on.

An upload never fails the action that produced the file. A terminal session that ended because object storage was briefly unreachable would be a developer's work interrupted by a storage problem they cannot see and did not cause; a backup CronJob that failed for the same reason would tell a customer they have no backup when the file is sitting on the volume where it always was. The file is written locally first and the upload retries. What must not happen is silence: the pending count is a metric and a line in the dashboard, because evidence that never arrives is evidence the customer does not have, and they need to know which.