Skip to content

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.


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.

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.

The signature covers the base64url payload segment exactly as transmitted, and the verifier checks that string’s bytes before parsing them.


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.

StepAction
1Generate a new keypair with a new kid
2Commit the new .pub and keep the old one
3Point LOGRITE_LICENSE_SIGNING_KID at the new key
4Re-issue on renewal; old licences verify until they expire naturally
5Delete 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:

ControlWhat it buys
13-month TTL ceiling, enforced at signingA leaked key cannot mint a perpetual licence
Every issuance audited before the licence is returnedWritten 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-TokenNot 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 licenceA 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.


Past expiresAt the install enters a 30-day read-only window: existing data stays queryable, ingestion and analysis stop. After that, entitlements close.


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.


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.


VariableWhereMeaning
LOGRITE_BILLING_MODE=onpremthe installForces NoopBillingProvider; entitlements come from the licence
LOGRITE_LICENSE_PATHthe installPath to the licence file
LOGRITE_LICENSE_STATE_PATHthe installClock watermark; defaults to <licence path>.seen
LOGRITE_LICENSE_SIGNING_KEYthe issuer onlyPKCS#8 private key, base64
LOGRITE_LICENSE_SIGNING_KIDthe issuer onlyMust match a committed .pub filename
LOGRITE_ADMIN_TOKENthe issuer onlyGuards POST /internal/admin/licenses

Every other instance sets none of the last three.


Terminal window
# what licence is this install running under?
curl -H "X-Logrite-Admin-Token: $LOGRITE_ADMIN_TOKEN" \
https://<host>/internal/admin/license

The 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.