How licensing works
A TestVibe license is a signed text blob you paste into your own install. Nothing phones home — not at activation, not at run time, not ever.
Three facts that shape everything else
- Offline and signed. The license is an ECDSA P-256 / SHA-256 signed token. Your install carries only the public key and verifies the signature locally. An air-gapped site licenses exactly like a connected one — the only thing that has to cross the gap is a 32-character ID going out and a block of text coming back.
- Bound to one install. The signed claims carry your customer name and this install's install ID. A license issued for one install will not validate on another — the instance reports "This license was issued for a different install" and stays unlicensed.
- Self-hosted only. Licensing is skipped entirely on TestVibe's
cloud. The switch is
TV_SELF_HOSTED; when it is not set explicitly the app infers self-hosted fromTV_STORAGE_PROVIDER=http. Both the appliance and the Compose stack setTV_SELF_HOSTED=1for you.
What a lapsed license actually stops
Enforcement happens at one chokepoint — the same pre-flight every dispatch already goes through. So it is precise, and it is not a kill switch.
| State | What still works | What is blocked |
|---|---|---|
| Valid | Everything. | Nothing. |
| Grace | Everything, plus an amber banner in the app that a user can dismiss for the session. | Nothing. |
| Read-only | Sign-in, browsing projects, reading features, opening past runs, traces and reports, exporting, deleting. A red banner that cannot be dismissed. | New test runs, new AI generations, new load tests. |
Find the install ID
The install ID is generated by your own instance the first time it starts against its database, and it is stored there. It is not baked into the image, so two installs from the same download have different IDs.
Where it lives
A 32-character hexadecimal string in the single-row tv_instance table,
written once on first boot and never re-generated. On the appliance that table is
on the data disk, which is why the ID survives an upgrade that
replaces the OS disk.
0303e70865794216b70ac562a227da18
Three ways to read it
- The administration console (recommended). Open the console —
http://<ip>/adminorhttp://<ip>:5120on the appliance,http://<host>:5120on Docker Compose — sign in, and go to License. The install ID is shown on a copy line labelled "Install ID — send this to TestVibe to be issued a license", next to a status pill and, while unlicensed, a "N day(s) of grace left" note. - Inside the application. Sign in as an owner or admin and open Settings → License. This panel exists only on self-hosted installs. It shows the same install ID with a copy button, the live verdict and its reason, a Re-check button, and a paste box.
- The log. The licensing service prints one line per check,
always ending with the install ID.
appliance
sudo journalctl -u testvibe-web | grep '\[license\]' | tail -3
docker composedocker compose logs web | grep '\[license\]' | tail -3
expected output[license] WARN grace — No license is installed. Activate a license within 13 day(s) to avoid interruption. (install 0303e70865794216b70ac562a227da18)
kubectl logs deploy/testvibe-web | grep '[license]'. The console is
reachable through the chart's admin service on 5120 as well.
tv_instance directly, so "Could not read this instance's identity" means
the database is unreachable or the migrations have not run. Fix that first; there is
no install ID to copy until the schema exists.
Issue a license
Licenses are minted on the customer portal, by you, against your own subscription. There is no ticket to open and no waiting.
- Open the portal and sign in with the email on your subscription — the same 6-digit-code sign-in you used for this page. If you are reading this, you are already signed in.
- Go to the License tab. The header states your entitlement: deployments used out of your quota, how many remain, your edition, and whether the subscription is perpetual or has an end date.
- Paste the install ID into the Install ID field. Add a Label — "Production", "DR site", "QA" — so the deployment list stays readable a year from now. The label is for you; it is not part of the signed claims.
- Press "Issue license". You get back a block of license text and a SHA-256 checksum of it. Copy the whole license text — it is one long token, and leading or trailing whitespace is trimmed for you on paste.
- Check the confirmation line. A first activation says "Licensed". Re-issuing for an install you already own says "Re-issued for an install you already own — this did not use another deployment." That distinction is the one worth reading.
Install it
Two routes, deliberately. Click-ops shops paste it into the console; infrastructure-as-code shops pin it in the environment and keep the box reproducible.
Route A — the console
The normal path, and the only one an appliance operator has, because an appliance has no OS login.
- Console → License → the Activate a license box.
- Paste the license text and press Activate. It is stored in the database, on the data disk, alongside the install ID.
- Watch the status pill flip to Valid. If it does not within a minute, see Troubleshooting.
A stored license can be removed again with Remove stored license — the panel also shows when it was last changed.
Route B — the environment
For Compose, Helm and anything else where the deployment file is the source of truth.
TV_LICENSE=<the license text from the portal>
Or keep it out of
docker inspect and out of your environment listing entirely by
mounting it as a file:
TV_LICENSE_FILE=/run/secrets/testvibe-license
Restart the web service after changing either one — the environment is read at process start.
How fast does it take effect?
Faster than the hourly re-check would suggest, because the paste path has its own signal. The application watches for the license row changing every 20 seconds and re-evaluates the moment it moves; the full re-check runs hourly on top of that.
- Console paste: live within about 20 seconds. The app's header banner refreshes on its own poll, so it clears without a page reload.
- Environment variable: at the next restart of the web service, because that is when the environment is read.
- Impatient? Settings → License → Re-check in the app forces an immediate evaluation.
Which license wins
If a license exists in more than one place, exactly one of them is in effect, and the rule is the same rule every other setting on this install follows: the environment is the floor and cannot be overridden from a web page.
| Priority | Source | Wins over |
|---|---|---|
| 1 | TV_LICENSE_FILE — the contents of the file it points at, on
the web service. |
Everything below. |
| 2 | TV_LICENSE — the environment variable on the
web service. |
The pasted license. |
| 3 | The license pasted into the console and stored in the database. | Nothing. |
TV_LICENSE is set does nothing
visible. The paste is stored, and then ignored. The console detects this
and prints a lock notice on the License panel — "TV_LICENSE is set
in this instance's environment, which takes precedence over anything pasted here.
Replace it where the environment is defined." If you see that notice, edit
your .env / Helm values / testvibe.env instead and
restart the web service.
The console can only inspect its own process environment. On both shipped shapes
that is the same environment the application sees — the Compose stack shares one
.env, and the appliance's console and web units both read
/etc/testvibe/testvibe.env — so the notice is accurate. On a bespoke
deployment where the two processes are configured separately, trust the web
service's environment, not the notice.
Grace and read-only
You are never locked out of a fresh install. An unlicensed instance runs normally for a fixed window first — long enough to evaluate, not long enough to run a business on.
The rules, precisely
- The clock starts when the instance first notices it is unlicensed — not when you downloaded the image, and not at midnight. The moment is written to the database, so restarting the box, reverting a snapshot of the OS disk or changing the system clock does not extend it.
- The window is 14 days by default, set by
TV_LICENSE_GRACE_DAYS. The console and the in-app banner both count down in whole days remaining. - Read-only pauses three things: new test runs, new generations, new load tests. That is the entire list. Reading, exporting and deleting are never gated — deleting in particular, because it is the recovery path for a full disk.
- Activating clears the clock. The stored "first unlicensed" timestamp is wiped on a successful validation, so if the license later expires you get a full fresh grace window rather than an already-spent one.
Expiry
A perpetual license has no expiry date and simply stays valid. A term license carries one in its signed claims, and the console shows it as "Licensed to <you> until YYYY-MM-DD".
- On the day it passes, the instance reports "The license expired on YYYY-MM-DD" and enters grace — a full 14-day window, because the clock was cleared while the license was valid.
- Expiry is evaluated on the hourly cadence, not to the minute. A license does not stop working at a precise second.
- Renew by issuing a new license for the same install ID on the portal. That is a re-issue, so it does not consume another deployment.
Rebuilds and seats
This is the question that decides how freely you can operate the box, so it is worth being blunt about it: rebuilding does not cost you a deployment, as long as the data survives.
What consumes a deployment, and what does not
| You do this | Install ID | Deployment count |
|---|---|---|
| Upgrade the appliance — replace the OS disk, keep the data disk | Unchanged | No change |
| Pull new container images, keep the Postgres volume | Unchanged | No change |
| Restore a database backup onto a new host | Unchanged | No change |
| Re-issue the license for an install ID you already activated | Unchanged | No change — free, explicitly |
| Start again on an empty database (new data disk, wiped volume, clean install) | New | Consumes one |
Second environments
A staging or DR instance is a separate install with its own install ID, so it needs its own license and does consume a deployment. Check the portal's License tab for what your subscription allows before you build it — the header shows "N used / M" and how many remain.
A DR instance restored from the production database is not a second install — it carries the same install ID and the same license. Do not run both at once expecting them to be distinguishable; they are not.
Reference
Every licensing variable the application reads. All of them are environment-only — none of them appear in the console's settings catalog.
| Variable | Default | What it does |
|---|---|---|
TV_SELF_HOSTED |
1 in both shipped stacks |
Marks this an on-prem install. Turns licensing on and
credit gating / token metering off. If unset, the app
infers self-hosted from TV_STORAGE_PROVIDER=http. |
TV_LICENSE |
unset | The signed license text. Beats anything pasted in the console. |
TV_LICENSE_FILE |
unset | A path to a file containing the license text — the Docker/Kubernetes
secret-mount form. Beats TV_LICENSE. |
TV_LICENSE_GRACE_DAYS |
14 |
Length of the grace window in days, from when the instance first noticed it was unlicensed. |
TV_LICENSE_CHECK_MINUTES |
60 |
The full re-evaluation cadence — the one that rolls grace into read-only and notices an expiry. |
TV_LICENSE_PROBE_SECONDS |
20 |
How often the app checks whether the stored license changed, so a paste in the console takes effect in seconds rather than in an hour. |
On the appliance these go in /etc/testvibe/testvibe.env, which both the
web service and the console read. There is no OS login on the appliance, so in
practice appliance operators use the console and leave this file alone.
Troubleshooting
The instance always tells you why it is unlicensed, in one sentence, in the console and in the log. Start there.
| What it says | What it means | Fix |
|---|---|---|
| "No license is installed." | Nothing pasted, nothing in the environment. | Follow Issue a license. |
| "The installed license is invalid or could not be verified." | The signature did not check out — almost always a truncated or re-wrapped paste, or text pasted from a document that changed the characters. | Copy it again from the portal with the copy button and paste as plain text. Compare the portal's SHA-256 checksum against what you pasted if you want certainty. |
| "This license was issued for a different install (…)." | Right customer, wrong install ID — usually a license from a previous clean install, or from your other environment. | Read the current install ID (section 02) and issue a license for that one. It is free if you already own the install. |
| "The license expired on YYYY-MM-DD." | A term license passed its end date. | Re-issue for the same install ID. Grace restarted at full length, so you have room. |
| Pill says Stored (not yet re-checked) | The console saved the license but has not been able to ask the running application for its verdict. | Wait ~20 seconds and reload, or use Settings → License → Re-check in the app. |
| Everything looks right, but the pill never turns green | TV_LICENSE is pinned in the web service's environment and
the paste is being ignored. |
See Which license wins. |
Where to go next
- The installation guide — getting the box up in the first place, and the console's five-item setup checklist.
- Sign-in · AI providers · Storage · Integrations · Capacity — the rest of the configuration.
- support@testvibe.com — if a license will not validate and none of the messages above match, send us the install ID and the exact reason text.