Expiry and Clock Integrity
The three enforcement layers#
License expiry is enforced in three places, each with a deliberately different posture.
1. Startup refusal#
In prod, a backend process will not start with an expired (or missing, or invalid) license. This is the hard gate: a restart after expiry requires a renewed license file.
2. API read-only mode#
A running API whose license expires is not killed — it drops into read-only mode:
-
GET,HEAD, andOPTIONSrequests pass unchanged. -
These paths pass regardless of method:
/health,/metrics,/api/v1/license, everything under/api/v1/auth/(login, refresh, logout, password change, SSO), andPOST /api/v1/admin/audit/verify-chain(a diagnostic read that happens to be a POST — an assessor can prove audit-chain integrity while the instance is read-only). -
Every other write returns 403 with
code: "license_expired":{ "detail": "License expired — deployment is in read-only mode. Reads still work; writes are blocked. Contact your vendor for renewal.", "code": "license_expired" }An invalid (rather than expired) license produces the same 403 with a detail pointing at the license file instead of at renewal.
The gate's license state is cached for 5 minutes, so enforcement begins within at most five minutes of expiry, and a fixed license is likewise picked up within five minutes (no restart needed to lift read-only — though feature gating still wants one).
3. Background workers#
Workers are never killed mid-job. A run that started before expiry
finishes; the next trigger is blocked at the API layer (which is
where jobs are started). The hourly re-check task logs the expiry and
flips the abrq_license_enforcement_active gauge, but deliberately does
not terminate in-flight work.
No grace period#
There is no grace period after expiry — this is a deliberate design
decision, not an oversight. A post-expiry grace window is ineffective
against trial abuse (it just extends the trial), so the warning happens
before expiry instead: the non-dismissible UI banners (warning at ≤30
days, critical at ≤7 days), the license_expiry_approaching log event,
and the optional expiry alert channel
(Notifications).
Warning. Plan renewal against the banner, not the expiry date. The day the license expires, every write API call starts failing with
license_expiredand scheduled work stops triggering.
Clock integrity#
Expiry checks depend on the host clock, so the platform keeps a
persisted monotonic anchor (license_last_seen) to make winding the
clock back unprofitable:
- The anchor only ever moves forward, and only advances after a
successful signature verification (at startup and on the hourly
re-check). On a fresh install it is seeded from the license's
issued_at. - Expiry is breached when
expires_atis at or before max(current clock, anchor) — a rolled-back clock cannot un-expire a license. - A small backwards drift is tolerated: the clock may lag the anchor by
up to
ABRQ_LICENSE_CLOCK_SKEW_SECONDS(default 3600 — one hour) without consequence, absorbing NTP corrections and restart jitter.
Rollback beyond the tolerance#
If the clock sits more than the skew tolerance behind the anchor, the
platform logs license_clock_rollback_detected and treats the license
as INVALID — API read-only mode, same as above.
Warning. A clock-rollback trip is classified as "something is wrong on this host — page someone", not "renew the license". A renewal will not clear it; fixing the clock will.
Recovery paths#
Rollback detection is designed to be recoverable without vendor involvement:
- Fix the clock. Correct the host clock forward to within the skew tolerance of the anchor (in practice: fix NTP), then restart. The condition self-heals once the clock is right.
- Last resort. An operator with framework-database access can reset the persisted anchor row — deliberately an operator action, never an API.
Never-brick guarantees#
The mechanism is built to never brick a legitimately licensed, correctly clocked host:
- The anchor cannot outrun real time: it advances only to verified "now", so normal operation never banks future time against you.
- If the anchor store is unreachable (database down at startup, table
missing mid-upgrade), the platform degrades to plain
current-clock expiry checking and logs a loud WARNING
(
license_last_seen_unavailable) naming the error and the degraded mode. A storage hiccup never refuses boot. - Skew inside the tolerance is absorbed silently; only a genuine rollback beyond one hour (or your configured tolerance) trips enforcement.