License a deployment
A deployment key is a machine credential that lets a server fetch its own licence, daily, for as long as your subscription is live. Configure it once and there is no renewal step for anyone to perform.
This page issues one, puts it in place for both deployment shapes, and then covers what each licensing state looks like from your side.
Read How licensing works first if you want the reasoning. This page is the procedure.
Before you start
Section titled “Before you start”- A deployment from either Docker Compose or Helm.
- An account at account.bugb.io with a subscription.
- A bearer token for the server, so you can read
GET /v1/licenseback. On a deployment that is not yet licensed you cannot mint one, because minting is a write: license it first, then mint.
The variables
Section titled “The variables”| Variable | What it is |
|---|---|
BUGB_DEPLOYMENT_KEY |
The one to set. A machine credential, bugb_dk_live_…, issued in the portal. The server uses it to fetch its own licence daily |
BUGB_LICENSE_TOKEN |
A pinned token, three base64url segments separated by dots. For deployments that cannot reach the portal. Setting it disables refresh |
BUGB_LICENSE_PUBLIC_KEY |
Not read by a published image, which verifies against the key stamped into it. Harmless to leave set; the server logs one line saying it is ignoring it. It is still the key a development build reads |
BUGB_PORTAL_URL |
Where the portal lives. Empty uses the URL compiled into the image (https://account.bugb.io). Set it only for a staging estate or an internal mirror |
Issue a key and install it
Section titled “Issue a key and install it”-
Issue the key.
Go to account.bugb.io/dashboard/deployment-keys and issue one. It is shown once, at issue. If it is lost, revoke it there and issue another.
It is a bearer credential, so treat it like the database password. Its only power is to fetch the current licence for its own account.
That page also shows, per key, when the deployment was last seen, the IP it came from, and the image version it reported. That is where to look first when a licence is not behaving.
-
Put it where the server reads it.
Under Compose, in
deploy/.env:BUGB_DEPLOYMENT_KEY=bugb_dk_live_xxxxxxxxBUGB_LICENSE_TOKEN=Under Helm, in a Secret you create yourself:
Terminal window kubectl -n bugb create secret generic bugb-license \--from-literal=BUGB_DEPLOYMENT_KEY='bugb_dk_live_xxxxxxxx'then name it with
--set license.existingSecret=bugb-license. -
Restart, and read the licence back.
Terminal window docker compose -f deploy/docker-compose.yml up -dcurl -s $URL/v1/license -H "Authorization: Bearer $TOKEN"{"mode": "licensed","product": "bugb","schema_version": 1,"tier": "team","state": "valid","active": true,"expires_at": "2027-09-03T00:00:00+00:00","seats": 25,"seats_used": 2,"features": [],"repo_limit": null}"state": "valid"and"active": trueis the answer you want.expires_atmoves forward on its own from here: the server fetches a fresh 30 day term every day for as long as the subscription is live.A brand-new deployment licenses itself within seconds of its first boot, and there is a short window before its first poll returns in which it answers
402. That clears on its own, without a restart.
There is nothing further to do. No renewal, no diary entry, no second visit to the portal. A cancellation takes effect on its own about 37 days later, which is the 30 day term plus the grace window.
Air-gapped deployments
Section titled “Air-gapped deployments”Leave BUGB_DEPLOYMENT_KEY unset and set BUGB_LICENSE_TOKEN to the pinned
licence you were issued.
With no deployment key configured the server builds no HTTP client and makes no outbound request of any kind. This is a supported deployment, not a degraded one. Those licences are issued on a longer term through your engagement, because a token that cannot refresh cannot be renewed silently either.
Moving a pinned deployment to refresh is one change: set
BUGB_DEPLOYMENT_KEY and clear BUGB_LICENSE_TOKEN.
The states a deployment can land in
Section titled “The states a deployment can land in”GET /v1/license is the diagnostic. Any valid bearer token can read it, and it
is never licence-gated: the route that answers “why am I getting a 402?” must
not answer with another one.
mode / state |
Writes | What it means |
|---|---|---|
licensed, valid |
accepted | normal |
licensed, grace |
accepted | past expires_at, inside the grace window |
licensed, expired |
402 |
past the grace window. Reads keep working |
licensed, state: null, active: false |
402 |
a licence is configured, or a key is, and nothing verified. See below |
dev, state: null |
accepted | a source build with no public key set. Not reachable from a published image |
Inside the grace window
Section titled “Inside the grace window”Writes still work, which is the point: a renewal stuck in procurement is not an outage.
{ "mode": "licensed", "product": "bugb", "schema_version": 1, "tier": "team", "state": "grace", "active": true, "expires_at": "2026-09-06T19:23:31+00:00", "seats": 25, "seats_used": 2, "features": [], "repo_limit": null}POST /v1/events -> HTTP 200The window is a quarter of the term, capped at 14 days. An annual licence gets the full fortnight; a 30 day one gets seven and a half.
An expired licence
Section titled “An expired licence”{"detail":"license expired: acme's team plan expired at 2026-06-03T00:00:00+00:00 and the grace window ended at 2026-06-17T00:00:00+00:00"}HTTP 402Reads are unaffected at the same moment:
GET /v1/repos -> HTTP 200An expiry degrades a deployment rather than stopping one. Everything already pushed stays readable; what stops is filing anything new.
A licence that did not verify
Section titled “A licence that did not verify”Three different causes produce the same /v1/license body, and the 402
detail is what tells them apart.
{"mode":"licensed","product":null,"schema_version":null,"tier":null,"state":null,"active":false,"expires_at":null,"seats":null,"seats_used":null,"features":[],"repo_limit":null}A deployment key that has fetched nothing:
{"detail":"license malformed: no license token configured; set BUGB_LICENSE_TOKEN"}HTTP 402A mangled token:
{"detail":"license malformed: expected 3 dot-separated segments, got 1"}HTTP 402Copy BUGB_LICENSE_TOKEN again, whole and on one line.
A licence Bugb did not sign:
{"detail":"license bad_signature: signature does not verify against this key"}HTTP 402A published image verifies against the key stamped into it, so this also names the case where a licence was signed under a key generation no released image carries yet.
Other refusals that are not licensing
Section titled “Other refusals that are not licensing”Two answers look like licensing problems and are not.
{"detail":"role 'viewer' cannot push events; member or above is required"}HTTP 403A 403 is the token’s role, not the deployment’s licence. Mint the caller a
member token.
{"detail":"missing credential"}HTTP 401A 401 is a missing or invalid bearer token. 402 is the only status that
means the deployment’s licence.
What a customer receives, and what to take back
Section titled “What a customer receives, and what to take back”Publishing the server image privately put a second credential on both halves of this, and the second half is the one people forget.
| Handed over at onboarding | Taken back at offboarding |
|---|---|
a registry credential for ghcr.io/bugb-technologies/bugb-server |
revoke it. Until it is revoked, a former customer can still pull every image ever published, including new ones |
a deployment key bugb_dk_live_…, or a pinned licence token |
revoke the key in the portal. Refresh stops returning tokens and writes stop about 37 days later; a pinned token runs out |
The two revocations are independent and neither implies the other. A lapsed licence stops writes but does not stop pulls; a revoked pull credential stops new deployments but does nothing to a running one, which already has the image on disk. Offboarding is both.
Related
Section titled “Related”- How licensing works: why refresh does not weaken the offline guarantee.
- Connect the CLI to a server:
what a
402looks like to a developer runningbravos push.

