On-prem licensing
An on-prem customer cannot be billed by usage that phones home, and cannot have entitlements
resolved by a control plane it cannot reach. So LOGRITE_BILLING_MODE=onprem replaces both: the
spend gate meters locally and never transmits, and a signed licence file becomes the entitlement
source in place of workspace_subscription.
Why not the payment provider’s licence keys
Section titled “Why not the payment provider’s licence keys”This was settled by reading the API rather than left unexplored.
A Polar licence key is a bare UUID4 with an optional vanity prefix. It carries no payload, no
expiry, no entitlements and no signature — there is nothing in the string to verify. Expiry,
activation count and usage quota are server-side counters, so validating one is necessarily an
online call to POST /v1/customer-portal/license-keys/validate. The words “offline” and “air-gap”
appear zero times across all 94 of their documentation pages.
The artifact
Section titled “The artifact”LOGRITE-<base64url(payload)>.<base64url(signature)>The payload carries the workspace (or null for a site licence), the plan code, an entitlement
snapshot, a model-registry snapshot, the seat ceiling, issuedAt, expiresAt, a kid and a
nonce.
The registry snapshot rides inside for the same reason as everything else: /internal/billing/model-registry
is unreachable on-prem. On-prem also gets a deliberately different set — with a customer-supplied
key the cheapest-model logic that drives SaaS margin does not apply, so the licence names a
permitted set rather than a cost-optimised one.
Ed25519, with no new dependency
Section titled “Ed25519, with no new dependency”The JDK has had EdDSA built in since 15; this project targets 17 and runs on 21. Signature.getInstance("Ed25519")
needs nothing from Bouncy Castle — which matters for an artifact a customer’s security team has to
be able to audit.
Verify the bytes that were signed
Section titled “Verify the bytes that were signed”The signature covers the base64url payload segment exactly as transmitted, and the verifier checks that string’s bytes before parsing them.
Key rotation
Section titled “Key rotation”Public keys ship in the jar as licensing/keys/<kid>.pub, and each licence names the key that
signed it. Several can ship at once, so a new key can start signing while every install already in
the field still verifies licences signed by the old one.
Without kid selection, rotating would invalidate every licence in the field simultaneously —
which is the same as having no rotation story.
| Step | Action |
|---|---|
| 1 | Generate a new keypair with a new kid |
| 2 | Commit the new .pub and keep the old one |
| 3 | Point LOGRITE_LICENSE_SIGNING_KID at the new key |
| 4 | Re-issue on renewal; old licences verify until they expire naturally |
| 5 | Delete the retired .pub only once every licence it signed has expired |
Step 4 is what makes rotation cheap. If a key is believed compromised, delete its .pub and ship
that build — every licence it signed stops verifying at once.
Full ceremony, including the openssl commands: logwrite-api/src/main/resources/licensing/keys/README.md.
Issuance, and what the controls actually promise
Section titled “Issuance, and what the controls actually promise”The signing key is env-injected into the backend, so anyone who reaches the production environment can mint a licence. No amount of application code changes that. The controls exist because it is true:
| Control | What it buys |
|---|---|
| 13-month TTL ceiling, enforced at signing | A leaked key cannot mint a perpetual licence |
| Every issuance audited before the licence is returned | Written to the hash-chained GovernanceAuditService, which has a verifyChain() — a quiet mint leaves a detectable gap rather than no trace |
A distinct X-Logrite-Admin-Token | Not the metering secret three services already hold, and not a workspace role — a licence can cover every workspace, so no workspace-scoped role should reach it |
kid on every licence | A suspected leak is answered by rotating and re-issuing, not by invalidating the field |
An instance with no signing key answers status: "DISABLED" rather than failing. That is a
correctly configured instance for every deployment except the one that issues licences.
Expiry degrades; it does not stop
Section titled “Expiry degrades; it does not stop”Past expiresAt the install enters a 30-day read-only window: existing data stays queryable,
ingestion and analysis stop. After that, entitlements close.
Clock rollback
Section titled “Clock rollback”Everything here is time-bound, and on-prem the customer owns the clock. Winding it back a year would otherwise revive an expired licence.
The highest issuedAt ever seen is persisted as a watermark — in a file beside the licence rather
than a database row, so it survives a restore from an older backup. A licence issued before the
watermark, or a system clock that has moved behind it, is refused.
Entitlement resolution reverses one rule
Section titled “Entitlement resolution reverses one rule”Everywhere else in EntitlementService a missing answer fails open: a wrong “no” takes a
feature from a paying customer, while a wrong “yes” costs at most an upsell.
On-prem, an absent licence reverses that reasoning. It is the one case where the customer has demonstrably not bought anything, so it resolves to the smallest plan rather than the internal unlimited one — otherwise deleting a file would hand over the entire product.
Configuration
Section titled “Configuration”| Variable | Where | Meaning |
|---|---|---|
LOGRITE_BILLING_MODE=onprem | the install | Forces NoopBillingProvider; entitlements come from the licence |
LOGRITE_LICENSE_PATH | the install | Path to the licence file |
LOGRITE_LICENSE_STATE_PATH | the install | Clock watermark; defaults to <licence path>.seen |
LOGRITE_LICENSE_SIGNING_KEY | the issuer only | PKCS#8 private key, base64 |
LOGRITE_LICENSE_SIGNING_KID | the issuer only | Must match a committed .pub filename |
LOGRITE_ADMIN_TOKEN | the issuer only | Guards POST /internal/admin/licenses |
Every other instance sets none of the last three.
Verifying an install
Section titled “Verifying an install”# what licence is this install running under?curl -H "X-Logrite-Admin-Token: $LOGRITE_ADMIN_TOKEN" \ https://<host>/internal/admin/licenseThe real test is offline: air-gap a container with no egress and confirm that a valid licence
resolves entitlements, a tampered one is rejected, a clock-rolled-back host is rejected, expiry
degrades to read-only, and verifyChain() reports intact after a sequence of issuances.