Licensing

How licensing works

The full lifecycle of your license, from the activation code you paste into helm install to the renewals that keep your deployment serving.

How licensing works

Your deployment runs under a license issued by the Solvegio Hub. The license is a signed token that lives in a Kubernetes Secret in your namespace. A small Deployment called the license agent obtains it, keeps it fresh, and writes every successor back into that Secret.

Everything below happens inside your cluster, driven by outbound HTTPS calls. Nothing is pushed to you.

You get a code

We mint a one-time activation code for your deployment. You pass it to the chart:

helm install my-mvno solvegio/platform \
  --set platform.kind=mvno \
  --set platform.host=platform.example.com \
  --set activation.code=<code-from-the-hub-console>

activation.code is the one licensing value you set. The chart writes it into the platform Secret (<release>-platform-env) under ACTIVATION_CODE.

The code is single-use. It also carries a fingerprint of the Hub's own signing key, packed into the code string. The agent checks that fingerprint against the key set published by activation.parentUrl before it enrolls, so a code minted for you cannot be redeemed against an impostor Hub.

The agent generates a key and enrolls

The license agent (<release>-platform-license-agent) starts alongside the API. On its first pass it finds no license in the Secret, so it enrolls.

It generates a 32-byte Ed25519 seed with the cluster's own randomness, derives a keypair from it, and posts the activation code plus the public half of that key to POST /v1/deployments/activate on the Hub. The private half stays in your namespace. See Your key never leaves for what that guarantee rests on.

The Hub redeems the code and mints a license bound to the key you presented. Binding means the license carries a cnf claim holding the thumbprint of your public key. A license with a cnf is worth nothing to anyone who does not hold the matching private key, because every later call has to prove possession of it.

The agent then writes the result back into the platform Secret. Six keys, and this is the complete set the agent ever owns:

Secret keyWhat it holds
PARENT_LEASE_TOKENThe license itself
SIGNING_SEEDThe seed the enrollment key was derived from
POP_SIGNING_SEEDThe seed of the key the current license is bound to
PARENT_BASE_URLThe Hub the deployment is pinned to
REQUIRE_PARENT_LEASESet to true, which is what makes the API fail closed
ACTIVATION_CODE_CONSUMEDThe code that was spent, so the same one is never replayed

A revoked deployment carries a seventh, LEASE_STATUS, which the agent sets and then clears again on recovery. See Revoke and recover. Every other key in that Secret comes from the chart or from you.

The API Deployment has been sitting behind a wait-for-lease initContainer this whole time. PARENT_LEASE_TOKEN landing in the Secret is what releases it. An unlicensed deployment never serves a request.

From then on, it renews

The license has a finite term. The Hub sets it (two weeks, by default) and stamps it into the token's exp claim, so you can always read your own expiry out of the token you hold.

The agent renews well before that. It calls POST /v1/licenses/{jti}/renew, signing the request with the key the current license is bound to. That signature is the proof of possession: only the holder of the private key can produce it.

Every renewal also rotates the key. Before it calls, the agent generates a new keypair and puts that new key's thumbprint in the request body. The Hub verifies the proof made with the old key, mints a successor license bound to the new one, and returns it. So each renewal is two things at once: an extension of the term, and a handover to a key that has never been used before.

The agent then persists both halves of the result, the successor license and the seed of the key it is bound to, in a single patch to the Secret.

Why the write-back is the whole point

This is the part that matters operationally.

The lease in your Secret is always the most recent one the agent obtained, not the one you installed with. A pod that restarts eight months after install reads whatever the agent last wrote. It finds a current license, bound to a current key, and it boots and serves.

The API does not even need the restart. It mounts PARENT_LEASE_TOKEN and POP_SIGNING_SEED from the Secret at /etc/platform/lease and /etc/platform/pop_seed and watches those files, so a rotation the agent performs at 3am is picked up in place, with no rollout and no dropped requests.

Grace, and what happens when the Hub is unreachable

Renewal fires early in the license term, leaving most of the term unused. That gap is deliberate: it is the grace window.

If the Hub is briefly unreachable, the renew attempt fails, the agent retries with exponential backoff, and your deployment keeps serving the whole time on the license it already holds. The failure is not a licensing event, it is a network event, and it costs you nothing until the license you hold actually expires. The window is wide enough to absorb an outage on either side without an operator noticing.

The agent publishes license_expiry_seconds on its metrics port, which is the number to alert on if you want warning. See Observability.

Fail closed

If there is no valid license, the deployment does not serve business traffic.

  • No license in the Secret at boot: the wait-for-lease initContainer never completes, and the API pod never starts.
  • License present but expired, or cleared because the Hub revoked it: every gated business route answers 424 Failed Dependency. The body says parent lease not loaded or parent lease revoked depending on which it is.
  • /health keeps answering, in every one of those states. The process does not crash and Kubernetes does not restart-loop it. A deployment without a license is quiet, not broken, and your data is untouched.

Requests that fail authentication still get their usual 401 first. The license gate sits behind auth, not in front of it.

Where to go next

Copyright © 2026