Skip to main content
Version: v5.7.0

Entitlements and licensing

Included from Basic tier

The entitlement service is the server's central licence enforcement: it resolves the effective entitlements — the embedded Basic-tier defaults plus the stored, signature-re-verified licence documents — and answers every create gate, feature gate and expiry-stop check through one component. Stored envelopes are re-verified on every resolution (never trusted as a stored validity flag); a document that fails re-verification contributes nothing and is counted in the diagnostic invalid_documents, and one named by the embedded revocation denylist contributes nothing and is counted in denied_documents. Resolution is cached for 15 seconds and additionally expires at the next expiry-state boundary, so a licence never reads "valid" past its clock boundary.

The licence file​

A licence file is a signed envelope: {payload, signature, key_id}, where the signature is Ed25519 over the canonical JSON of the payload and key_id selects the verification key from the keyring embedded in the binary (pkg/licence/keyring.json). The package holds verification only — signing lives in a separate utility LimePoint holds, built on the same package so the two sides cannot drift format. Verification failures are named sentinels:

  • licence: malformed licence file
  • licence: unknown signing key
  • licence: signature verification failed

Expiry is deliberately not a verification failure: it is computed state, so an expired licence applies cleanly and reports its state — which is what makes renewal painless.

Every keyring key carries a role that scopes what it may sign: an issuing key (the default for an entry without a role) signs licences of every tier; a registration key signs only Basic-tier self-service documents (any other tier under it is a signature failure); a development key signs anything an issuing key signs and labels the signer as the dev/demo keypair — the committed dev/demo licences (examples/licences) are signed by the 2026-test development key, which only development builds of the binary carry (opschain version prints dev keys: on on such a build). A released binary trusts production keys alone, so a dev/demo licence answers licence: unknown signing key on it; evaluate a release with the evaluation licence shipped beside it. A mistyped role makes the keyring invalid rather than widening a key's scope.

The Basic-tier registration lane​

A server with no licence document stored runs on the evaluation footprint — 1 project, 1 environment, 1 asset, 1 runner, 1 runner group, no agents, no GitHub integration, no Academy — and every status surface carries a persistent registration notice. The Basic tier itself (1 project, 3 environments, 1 asset, 3 runners, 1 runner group, the GitHub integration with its Basic caps, the Academy's Foundations track) comes from a registered Basic licence, obtained by self-service registration on LimePoint's licence service: opschain admin licence register (or the registration page) captures a company email address and three declared fields (organisation, country as an ISO 3166-1 code, use case from a short list), shows the full list of what is collected and why, and records acceptance of the privacy notice (by version) with the registration — then applies the minted document to the server and stores its phone-home credential, so registration never means copying JSON by hand (opschain docs cli/admin-licence-register). Registering is free; the disclosure is its condition. A service whose registration lane is closed answers so by name — an evaluation licence from LimePoint is the other way up. The signed document that arrives is a Basic-tier, online-enforcement licence minted by the service's registration key — the one key that lives in the service, and the one role that can sign nothing but the Basic tier, so a compromised registration key can never produce a paid-tier licence. A released binary trusts the production registration key, so a registered document verifies on it; a development build additionally carries a development registration key for the lane's own tests.

Disclosed installation intelligence is a condition of that tier. For a Basic-tier online licence only, each verification round attaches a coarse platform profile beside the identifiers below: the operating system and architecture families, the CPU count, the installed memory as a coarse class (under 4 GB … over 64 GB, or unknown) and whether the installation runs in a container, a virtual machine or on a host — every value one of a short fixed list. The profile has no field for a hostname, a MAC address, a serial number or any other identifier, and the service refuses any such field by name. Paid tiers never send it: an Enterprise or pack licence's verification round carries the identifiers below and nothing else, and usage telemetry remains opt-in on both the licence and the installation.

The signed document payload carries licensee, licence_id, key_id, issued, expires, optional grace_days, tier, limits and features. Grace is licence-carried, never a server setting: a document without grace_days gets the default 30 days. A stored setting would let customers extend their own licences.

The legacy RSA surface​

A second, older verifier handles upstream-format RSA licence files ({data, signature}, RSA-SHA256 over the data object), verified against the embedded production public key oc_public_key.pem (an RSA public key — it verifies signatures, it cannot produce them). A legacy file accepted by PUT /api/licence is stored Base64-encoded in the licence key of the global settings blob and reported back as /api/info's licence_info attribute (the data object when valid, the exact sentinel licence file invalid otherwise, null when absent). PUT /api/licence routes by file shape: an envelope (payload/key_id present) goes to the entitlement service; anything else takes the legacy lane. licence_info keeps the upstream GUI contract; licence_status (below) is the enforcement truth.

Tiers and resolution​

Three tiers: basic (the embedded evaluation footprint when no licence is stored, or a registered Basic document), enterprise (replaces the defaults wholesale), and pack (layers packs:<name> entitlements beside a base licence).

Resolve is pure — the same documents and clock always resolve identically:

  • With no base document the embedded evaluation footprint applies and the state is valid — the footprint never expires; its grants are sourced evaluation and licence_status.registration names the way to the Basic tier.
  • A registered Basic document is a base document like an enterprise one: it replaces the footprint with the Basic limits and features (and runs 365 days, renewed over the phone-home channel); an enterprise document replaces the defaults wholesale (tier, limits, features, grace days); when several base documents are stored (a renewal re-mint beside its predecessor) the one expiring last wins.
  • Pack documents layer their features beside the base while valid or in grace, and stop contributing once expired.
  • An expired enterprise licence keeps its entitlements in the result, with the state carrying the stop signal — expiry never reverts a licensed system to the Basic tier; only explicit removal of the last enterprise document does.
  • The baseline union applies last: packs:common and identity:builtin are always granted, and packs:academy on every licensed resolution, whatever the applied documents carry (every licensed tier carries the built-in sign-in capability and the Academy agent packaging) — the mint tool also injects packs:common at mint time, and this union is the product-side belt for licences issued before it did, so applying an enterprise licence never silently revokes the bundled pack, the Academy packaging or the packaged sign-in buttons. The evaluation footprint unions packs:common and identity:builtin only: the Academy is one of the things registration lights up.
  • packs:academy is in the Basic tier — the registered Basic document carries it explicitly, and the union keeps it on every licensed resolution — and is the gate on any academy content: the Basic tier unlocks the Foundations track only, and every other academy track is owned by a pack and opens with that pack's own entitlement, which the Academy agent reads back from licence_status and enforces as a track gate, never the licence (the Academy pack, "What the licence unlocks").

The embedded evaluation footprint (pkg/licence/defaults.json, overridable at packaging time via a build overlay, never at deploy time): limits of 1 project, 1 environment, 1 asset, 1 runner, 0 agents, 1 runner group, 0 GitHub trust policies, 0 GitHub token exchanges and mints per day and 0 self-hosted GitHub instances; features assets, identity:builtin, packs:common, templates, triggers. The registered Basic document the licence service mints carries the Basic tier proper: 1 project, 3 environments, 1 asset, 3 runners, 0 agents, 1 runner group, 3 GitHub trust policies, 1,000 GitHub token exchanges and 1,000 mints per day, 0 self-hosted GitHub instances; features assets, identity:builtin, integrations:github, packs:academy, packs:common, templates, triggers.

Upgrading onto the footprint. An unregistered server that upgrades to this release lands on the footprint at once, with no grace window. Counts are create gates, so everything already in the estate stays — three environments and three runners keep working — and the next create above the footprint is refused by name, with the refusal naming opschain admin licence register first. Features lapse politely: the GitHub integration keeps its connections and trust policies, refuses new token exchanges, just-in-time mints, attestations and webhook provisioning with its named refusals and audit events, keeps deletion open, and lights back up the moment the registered document is applied.

The feature registry​

The fixed feature vocabulary (pkg/licence/features.go) is the single table the enforcement sites, validation and the features listing share:

FeatureGates
agentsfleet agent registration and connection — the bare feature packs:agents supersedes (either grants; the pack name is canonical on new licences)
assetscreating asset nodes (and the template-driven estate model with them)
identity:builtinregistering the built-in packaged sign-in providers as plain identity — GitHub (github.com), Google and Microsoft Entra ID on your tenant's issuer (https://login.microsoftonline.com/<tenant id>/v2.0, registered with --tenant-id; the multi-tenant common endpoint is not supported). Basic-available and baseline-unioned; GHES hosts are gated by tier instead.
identity:ssoenterprise SSO: customer-configured OIDC provider instances (your own IdP or tenant) and group→role mapping writes on them (an ldap-kind connection's mappings are base platform, never gated). Enterprise-weighted; a lapse refuses new registrations and mapping creates but retains configuration, keeps listings and mapping deletion open, and never touches local/LDAP login or the step-up floor.
integrations:githubthe GitHub-native integration suite's server side: GitHub App connections, trust policies, the Actions OIDC token exchange, just-in-time GH_TOKEN installation-token minting, run attestation and webhook provisioning (opschain docs integrations-github). Basic-available with limits as the lever — github_trust_policies (3 on Basic), github_token_exchanges_per_day (1,000 on Basic), github_token_mints_per_day (1,000 on Basic) and github_self_hosted_instances (0 on Basic: GitHub Enterprise Server is the Enterprise tier boundary, decided by host; github.com including GitHub Enterprise Cloud stays Basic-reachable). A lapse refuses new registrations, enabling writes, token exchanges, JIT mints, attestations and webhook provisioning with named messages (each an audit event), retains configuration untouched, and keeps deletion open — everything lights back up on upgrade. Sign-in with GitHub rides identity:builtin, never this feature.
integrations:jira-service-managementthe Jira Service Management integration's server side: JSM connections (Atlassian cloud or Jira Data Center, project-key scoped), the jsm:request / jira:issue typed-reference lane, Jira webhook provisioning on the signed inbound lane, and the single audited outbound write door (internal comments, transitions, field writes, evidence-linked resolution — opschain docs integrations-jsm). Enterprise-weighted, never in the Basic-tier defaults. A lapse refuses new registrations, enabling writes and every outbound request write with named messages, retains configuration untouched, and keeps deletion open — everything lights back up on upgrade.
maintenancemaintenance windows
secrets:multi-vaultmulti-vault secrets: enabling more than one secrets provider and declaring scope bindings. The first enabled provider is base platform and survives licence lapse; a lapse refuses new enabling writes and binding declarations but retains configuration — existing providers and bindings keep resolving, and binding deletion stays open. The runner lease ships ungated: security posture, never metered.
templatestemplate authoring
triggerstrigger creation

Beside the fixed registry, each signed commercial action pack names its own packs:<name> entitlement, validated by shape only. The canonical pack entitlements are product entitlements in their own right, each with its own reference page:

Pack entitlementPage
packs:academythe OpsChain Academy agent packaging (opschain academy: install the pinned agent release, run it against your server) and the gate on any academy content — in the Basic tier and always granted (the baseline union above); it unlocks the Foundations track only, and every other track is owned by a pack and opens with that pack's entitlement, read back by the agent — the Academy pack
packs:agent-governancethe agent-governance pack's gated surfaces — the agent-governance pack
packs:agentsthe agent platform's pack surfaces — the agents pack
packs:ai-agentsthe AI-agents pack (requires packs:agent-governance) — AI agents
packs:ai-agents-<domain>one specialised AI-agents pack per SME domain (e.g. packs:ai-agents-oracle), each licensed separately and requiring packs:ai-agents — the specialised AI-agent domain packs
packs:awsthe AWS content pack (opschain-pack-aws): the aws:* actions — the governed aws:api lane, the generated typed catalog and the EC2/S3 provisioning actions. Its signed manifest names requires: packs:aws — the AWS pack
packs:azurethe Azure content pack (opschain-pack-azure): the azure:* actions — the governed azure:api ARM lane, the generated typed catalog and the VM/blob/deployment actions. Its signed manifest names requires: packs:azure — the Azure pack
packs:bindings-<framework>one regulatory binding pack per framework's vocabulary (e.g. packs:bindings-dora), declared bindings onto the canonical governance themes — the regulatory binding packs
packs:change-governancethe change-governance pack's gated surfaces — change governance
packs:commonthe bundled starter pack — always granted, the baseline union above — the common pack
packs:infrastructurethe infrastructure pack: host-targeted actions against registered infrastructure host nodes and the provision hand-over; satisfies the packs:rpc gate — the infrastructure pack
packs:mintpressthe MintPress journey: the Ruby lane, MintModel actions and renders — MintPress pack composition
packs:ping-aicthe PingOne Advanced Identity Cloud content pack (opschain-pack-ping-aic, mounted as ping_aic): the ping_aic:* actions — the ping_tenant and ping_aic_* YAML resources with plan/apply/export, environment promotion, environment secrets and variables, the configuration tree, identities, service accounts, tenant logs and the double-gated ping_aic:api lane. Its signed manifest names requires: packs:ping-aic — the PingOne Advanced Identity Cloud pack
packs:rpcthe rpc pack: the rpc YAML actions and the transport-provider family, as one grant — the rpc pack
packs:secrets-awsthe aws-secrets-manager SecretsProvider kind — the AWS Secrets Manager provider
packs:secrets-azurethe azure-key-vault SecretsProvider kind — the Azure Key Vault provider
packs:security-governancethe security-governance admin and evidence surfaces — the security-governance pack

The pack entitlements required by the five signed pack repositories, as their manifests name them: opschain-pack-rpcu requires packs:rpc, opschain-pack-infrastructure requires packs:infrastructure (which satisfies the packs:rpc gate by composition), opschain-pack-aws requires packs:aws, opschain-pack-azure requires packs:azure, and the ping-aic pack of the opschain-pack-ping family requires packs:ping-aic. Further signed content packs (OCI and GCP automation content) are minted the same way, as packs:<name> entitlements validated by shape; each gains its reference page here as it ships.

Grant-side validation is strict — minting or defaults with an unknown name fail with unknown feature %q — valid features are agents, assets, identity:builtin, identity:sso, integrations:github, integrations:jira-service-management, maintenance, secrets:multi-vault, templates, triggers, or packs:<name> for a pack entitlement — but document verification is tolerant: issued licences may carry names this build does not gate (e.g. ha on licences minted before the registry existed), which are inert and reported as unknown_features in licence_status, never a verification failure.

List the vocabulary and the server's effective grants with opschain admin licence features (see opschain docs cli/admin-licence-features).

Tool entitlements (tools:<name>)​

Beside the pack vocabulary, a licence document may carry tools:<name> entitlements for standalone LimePoint tool binaries: the tool verifies the licence document offline and refuses without the grant — no OpsChain server involved.

Tool entitlementGates
tools:rpcuthe rpcu transport utility's user verbs — composed into pack-entitled licences at mint, or granted explicitly
tools:<name>the standalone LimePoint tool <name>: its binary verifies the licence document offline and refuses without the grant

Tool entitlements are composed in at licence minting: a licence granting packs:rpc, packs:infrastructure or packs:mintpress is minted with tools:rpcu alongside, and the tool's offline gate also accepts those pack entitlements directly, so a licence minted before the composition existed still drives the tool.

The server hands the licence to the tool at dispatch: a step whose command references the OPSCHAIN_LICENCE_DOCUMENT variable receives the applied licence envelope by value — the latest-expiring applied licence granting tools:rpcu or one of the pack entitlements that compose it — and the variable stays unset when no applied licence grants it, leaving the tool's own ambient licence gate in charge.

What each documentation page requires​

Every served documentation page — this reference tree, the resource and CLI dictionaries, and the getting-started runbooks — declares the licence entitlements its documented surface requires, as an entitlements: key in the page's front matter. The value is either the single value base, or a list of entitlement codes from the vocabulary on this page:

ValueMeaning
baseThe surface ships in every tier from the Basic tier up — no additional entitlement is required to use it.
[<code>, …]Using the documented surface requires the listed entitlements; a page documenting a mixed surface lists every relevant code, and a list that also carries base means the core surface is available from the Basic tier while the named codes gate the parts the page calls out.

Every code referenced by a page is defined on this page — in the feature registry, the pack entitlement table or the tool entitlement table above — so this page is the single registry the badges resolve against.

The gates​

Every gate reads the same resolution:

  • Node creates — the kind's feature entitlement first (asset is the one kind with a feature gate, assets), then its countable limit (projects / environments / assets, live non-deleted nodes).
  • Feature creates — triggers, maintenance windows, templates.
  • The agent plane — agent registration and session establishment require packs:agents (or the bare agents feature it supersedes), and the agents limit bounds concurrently connected instances, counted from the durable registry's liveness (it survives failover): existing connections stay live when the limit shrinks or the licence lapses; a NEW registration or reconnect refuses loudly with a named error plus a customer-visible warn:licence:feature_refused event (throttled per subject). AI-type enrolment (a manifest declaring type: ai) additionally hard-depends on packs:agent-governance + packs:ai-agents — the refusal names every missing pack; ungoverned AI cannot even enrol. packs:agent-governance gates the governance funnel over the base agent capability (engine in base, pack gates creation and surfaces): trust-grant create/approve/reject and the grants listing, the recommendations lifecycle, and the per-agent activity/trust-review reads — each a named refusal. Base and never gated: enrolment and the channel, tier enforcement on the request lane, assignments (the ADVISE floor's carrier), grant revoke (authority-shedding), every halt/drain scalpel, and the audit trail.
  • Pack use — a LimePoint-signed action pack whose verified manifest declares a required entitlement is gated on that packs:<name> feature; a pack with no manifest is never gated. The cloud content packs ride exactly this gate: the AWS pack on packs:aws and the Azure pack on packs:azure, each named by its signed manifest, refused at signed-bundle install and at every action run without the grant, with installed content and configuration retained across a lapse.
  • The Ruby lane (packs:mintpress) — this lane is only applicable to existing MintPress customers and those using the MintPress SDK or the packs:mintpress feature. Without the entitlement, everything that needs the opschain-core Ruby gem refuses, per step: creating a change whose frozen tree executes through the Ruby lane (the actions.rb discovery run, stored-catalog and MintModel actions — at the direct create, the retry, and the workflow change-step alike, every client — the server is the authority behind the GUI's greyed tabs); new MintModel renders (…/mintmodels/generate); and the actions.rb/MintModel catalog derive, refused at enqueue. The degrade is transparent: assets on MintModel-backed templates still create, re-pin and bulk-assign, and every YAML action file runs as normal. Reads stay up and nothing is silently hidden: stored mintmodels, mintmodel_history and template authoring keep working, and the action catalog keeps every entry listed — Ruby-lane entries marked entitlement_required, with the list's actions_rb_present/entitlement_required meta flagging a never-derived actions.rb catalog (a git-tree file-presence check, never Ruby execution) — so existing MintPress customers are made whole by licence issuance, never a code carve-out. Each refusal is a 422 naming the entitlement (v2: permission_denied with ERROR_REASON_FEATURE_ENTITLEMENT_REQUIRED) plus a throttled customer-visible warn:licence:feature_refused event (one per surface/node/hour). The runner fleet is enforced server-side: no MintPress work exists to dispatch without the entitlement, and registering a runner (or minting a registration token) into a group bound to MintModel-templated node paths emits the same warn event as a signal — never a refusal, since a group name is only convention.
  • Notification governance (packs:security-governance) — without the entitlement, the NotificationAdminService governance procedures (mandated-subscription administration, the delivery-evidence query, retention configuration) answer the typed ERROR_REASON_PACK_ENTITLEMENT_REQUIRED refusal naming the pack. Enforcement of existing mandates — the engine's matching, the member-verb refusals — and the retention reaper never consult the licence: the controls are ungated, the admin surfaces are the pack (the posture), so a lapsed entitlement freezes administration without dropping the org's guaranteed coverage. Retention additionally reads the tier: Basic is fixed at 30d feed / 90d evidence; an Enterprise licence makes both tunable (defaults 90d / 400d).
  • Step-up by policy (packs:security-governance) — setting require_step_up/step_up_on_read on an authorisation rule refuses without the pack (ERROR_REASON_PACK_ENTITLEMENT_REQUIRED); clearing a bit and every other rule write stay ungated. Enforcement of stored bits is likewise pack-gated with the polite-lapse posture: a lapsed estate keeps the bits but they go inert — signalled by a throttled warn:licence:feature_refused event (surface authorisation_rule_step_up, once per process-interval) — and the hard-coded step-up verb floor is never affected either way.
  • Cloud secrets-provider kinds (packs:secrets-aws / packs:secrets-azure) — creating a SecretsProvider of kind aws-secrets-manager or azure-key-vault, or enabling a disabled one, answers the typed ERROR_REASON_FEATURE_ENTITLEMENT_REQUIRED refusal naming the kind's pack. The polite-lapse posture applies: already-enabled providers keep resolving, reads and the test verb stay up, and disabling/deleting stays open for lapse cleanup — only the create/enable writes gate. The vault kinds (openbao, hashicorp-vault) are base platform, and the secrets:multi-vault gate composes independently on top (a second enabled provider of any kind). See the AWS Secrets Manager provider page and the Azure Key Vault provider page.
  • Transport providers (packs:rpc) — creating a transport provider, or enabling a disabled one, answers the typed ERROR_REASON_FEATURE_ENTITLEMENT_REQUIRED refusal naming the pack (packs:rpc natively grants the transport-provider family and the rpc YAML pack — no separate feature flag; bundles such as packs:mintpress compose it in at minting). The polite-lapse posture applies: stored records keep serving, reads and the test verb stay up, and disabling/deleting stays open for lapse cleanup — only the create/enable writes gate.
  • Infrastructure pack (packs:infrastructure) — the infrastructure action pack's surfaces: host-targeted rpc actions dispatched through registered infrastructure host nodes (a host_ref input re-points the transport session at the referenced host, with the transport grant enforced against the host node's path) and the provision hand-over that registers and destroys host nodes. Every licence granting packs:infrastructure is minted with packs:rpc (and packs:common) alongside, and the gate itself backstops the composition: a packs:infrastructure grant satisfies every packs:rpc gate, so a licence minted without the bundle still drives the pack's transport engine. The reverse never holds — packs:rpc alone does not grant the infrastructure pack.
  • Transport grants and bindings (packs:rpc) — creating or directly approving a transport grant, and declaring a scope binding, gate on the same pack with the same polite-lapse posture: stored grants keep serving dispatch, reads stay up, and reject/revoke and binding deletes stay open for lapse cleanup, so a lapsed estate can always unwind authority.
  • Runner registration — the runners limit against currently registered (unrevoked) runners.
  • Runner group creation — the runner_groups limit against distinct existing group names; joining an existing group is never refused, and a document minted before the limit existed (field absent) is unlimited.
  • GitHub integration (integrations:github) — connection and trust-policy creates and enabling writes check the feature; a non-github.com host counts against github_self_hosted_instances (never upliftable on paid Basic — the tier boundary itself), policy creates against github_trust_policies, each token exchange against github_token_exchanges_per_day per UTC day, and each just-in-time GH_TOKEN installation-token mint at step dispatch against github_token_mints_per_day per UTC day. All four are pointer-shaped: absent on a minted document means unlimited. Refusals are named with the uplift remedy and audit-evented; deletes never gate.
  • JSM integration (integrations:jira-service-management) — connection creates, enabling writes, webhook provisioning and every outbound request write (internal comment, transition, field write, evidence-linked resolution) check the feature; refusals are named and deletes never gate.
  • The work stop — see lifecycle below.

The refusals are named 422s, verbatim:

  • Limit: the <tier> licence allows <N> <kind>s (<M> in use) — apply a licence with a higher limit to create more (opschain admin licence apply)
  • Feature: <noun> require an enterprise licence with the "<feature>" feature — the <tier> licence does not include it (opschain admin licence apply)
  • Pack: action pack "<pack>" (mounted as "<mount>") requires the "<feature>" entitlement — the <tier> licence does not include it; apply the licence pack that grants it (opschain admin licence apply / PUT /api/licence)
  • Revoked on apply: licence <id> has been revoked by LimePoint (<reason>) and cannot be applied — contact LimePoint support for a replacement licence

Lifecycle: valid → in_grace → expired​

The expiry state is always computed from the document and the clock, never stored:

StateMeaningWhat it stops
validBefore expiry.Nothing.
in_graceInside the document's own grace window (its grace_days, default 30) after expiry.Nothing — everything works, loudly (warn:licence:expiring).
expiredGrace ended.Starting new work (changes, workflow runs, trigger firings, maintenance lifecycles, runner job offers). Reads, licence uploads and in-flight steps are never stopped.

The post-grace stop's refusal, verbatim: the licence for <licensee> expired on <date> and its <N>-day grace window has ended — no new <activity> are accepted until a licence is applied (opschain admin licence apply / PUT /api/licence); existing data, reads and in-flight steps are unaffected. On the dispatch plane the stop parks queued work with its own verdict — see the licence rung of opschain docs fleet-queue's verdict ladder. The boolean form of the stop (the runner offer gate) fails open on a resolution error, so a degraded database read never halts the fleet by itself.

Every replica runs a watch loop (default every minute) that makes the lifecycle loud, throttled durably against the events table (once per 6 hours across all replicas and restarts): warn:licence:expiring during grace, warn:licence:expired once stopped.

Surfaces​

SurfaceWhat
PUT /api/licenceMultipart upload (form field licence). Authorised as update on the /licence path (ordinary user auth, not the admin bearer). Envelope lane: named 422s for verification failures, 201 with the document plus the post-apply licence_status. Legacy lane: upstream 422s (No licence file provided, Licence has failed content validation, …), 201 with the validated data object.
DELETE /api/licence/{licence_id}Removes one stored document; 404 for an unknown id; 200 with the post-removal licence_status. The explicit act that can revert to the Basic tier.
/api/info → licence_statusThe resolved tier, state, effective limits and features, grace_days, unknown_features, the per-feature grants (evaluation sources the footprint), the registration notice while the footprint applies, every stored document with its own state, and the invalid_documents / denied_documents counts. Served beside the legacy licence_info.
LicenceService.StoreLicenceCredential (API v2)Stores one online installation's phone-home credential in the platform secret vault under licence/<installation_id>/credential — the last step of opschain admin licence register; the environment variable, when set, still wins for that installation. Authorised as update on /licence; never echoes the credential.
GET /admin/licence (admin bearer)The usage report the support bundle collects: store_kind (postgres or sqlite), licence_status plus used/limit per countable entitlement, counted by the same rules as the create gates. Degradations are named in notes, never silently omitted.
POST /api/licence/verify and the background roundThe online verification request to the licence service carries product, licence_id, installation_id, product_version and store_kind (postgres or sqlite). For a Basic-tier document it also carries the platform profile (os, arch, cpu_count, memory_class, runtime — closed vocabularies, see the registration lane above); paid-tier documents never carry it.

CLI: opschain admin licence register|apply|show|remove|features|verify — see opschain docs cli/admin-licence and its subpages (cli/admin-licence-register, cli/admin-licence-apply, cli/admin-licence-show, cli/admin-licence-remove, cli/admin-licence-features, cli/admin-licence-verify).

Storage​

Envelopes live in the licence_documents table on the scheduler database (PostgreSQL in server mode — HA-wide, shared by every replica — or SQLite), keyed by licence_id with the uploader recorded: re-applying the same licence replaces it, a renewal (new licence_id) accumulates beside its predecessor. A deletion of an unknown id is the named licence document not found. A server composition without a store refuses apply/remove with this server composition cannot store licences; GET /admin/licence on such a composition reports "enforcement": "not wired".

Events vocabulary​

All ordinary triggerable events, on the platform path:

  • api:licence:applied — every successful envelope apply (licence_id, licensee, tier, expires, actor).
  • api:licence:removed — every DELETE /api/licence/{id}.
  • warn:licence:expiring — during the grace window, throttled to once per 6 hours across replicas.
  • warn:licence:expired — once the stop is in force, same throttle.

Failure modes​

  • A tampered or wrongly-keyed file never stores: the 422 names the cause (Licence signing key is not trusted by this server, Licence signature verification failed — the file is not a genuine OpsChain licence or has been altered, Licence file is malformed — not a valid OpsChain licence).
  • A stored document that stops verifying (keyring change, tampering at rest) silently stops granting but is visibly counted in invalid_documents; a denylisted one in denied_documents.
  • Expiry is never a surprise: the grace window is loud (warn:licence:expiring), the stop is loud (warn:licence:expired), and queued work parks with a licence verdict instead of being lost.
  • A degraded entitlement read fails closed at create doors (the error surfaces) but open at the runner offer gate, so the fleet is never halted by a transient database fault alone.
  • opschain docs fleet-queue — the licence rung of the dispatch verdict ladder.
  • opschain docs cli/admin-licence — the admin licence command family.
  • opschain docs events — where the licence lifecycle events land.