Licence + activation
Three pieces: licence keys, activation tokens, and the device list. They cover three different “who-is-this” questions.
Licence key
Section titled “Licence key”A 16-character string with the QUAY-XXXX-XXXX-XXXX-XXXX shape
(letters + digits, no 0 O 1 I L to avoid OCR ambiguity).
The licence key is what you receive by email after a successful checkout. It’s tied to your email server-side, not to a specific machine. Lose it? Sign in to account.quay.uncle-z.com with the same email and copy it back.
Activation
Section titled “Activation”Pasting the licence key into Settings → Licence runs:
- App POSTs
/v1/activateto the licence server with the key- a stable per-machine fingerprint (hashed hardware UUID +
OS user) + a friendly device name (defaults to
Quay on <hostname>)
- a stable per-machine fingerprint (hashed hardware UUID +
OS user) + a friendly device name (defaults to
- Licence server checks the licence is
active(not revoked / suspended), checks the device count against the tier’s limit, issues an activation token signed with the server’s Ed25519 key - Token is returned + cached locally; offline tier-checks succeed
for 30 days from the token’s
issued_at
The activation token contains:
{ "license_id": "uuid", "license_key_hash": "sha256(key)", "tier": "pro", "device_id": "fingerprint", "device_limit": 2, "issued_at": "2026-05-09T…", "token_expires_at": "2026-06-08T…", "license_expires_at": "2026-06-09T…"}The token is <base64url(payload)>.<base64url(Ed25519 signature)>.
Quay verifies the signature locally on every launch using the
public key compiled into the binary. Tampering breaks the
signature; expired tokens trigger a heartbeat to refresh.
Heartbeat
Section titled “Heartbeat”Once a day (or on launch) the app POSTs /v1/heartbeat with the
current token. The licence server returns a fresh token reflecting
any changes (tier upgrade, device removed, expiry extended on
renewal). The fresh token’s license_expires_at always matches the
canonical row on the server, so a renewal that landed five minutes
ago is reflected in the activation by the next heartbeat tick.
If the heartbeat fails (network down, server hiccup) the existing
token stays valid until its token_expires_at (30 days). Past
that, the app falls back to Free tier until it can heartbeat
again.
Device limits
Section titled “Device limits”| Tier | Devices |
|---|---|
| Free | unlimited (no enforcement) |
| Pro | 2 |
| Pro Plus | 5 |
Add-on seats: extra activation slots can be purchased from the Account portal for either paid tier. The device list shows hostname + last-seen date + a “Deactivate” button per device. See Account → Devices.
Activating a third device on a Pro licence returns 409 with:
device limit reached (2); deactivate one in the account portalThe app surfaces this clearly with a one-click link to the portal.
Revocation
Section titled “Revocation”If a licence is revoked (refund processed, abuse, etc.):
- Server-side:
licenses.statusflips torevoked. Every device is dropped from the table. Future heartbeats from those device fingerprints return 401. - Client-side: the existing activation token stays signed until
its
token_expires_at, but the next heartbeat 401s and the app falls back to Free immediately. No grace period for revocations (unlike expiries, which respect the pre-paid period).
Where the keys live
Section titled “Where the keys live”| What | Where | Format |
|---|---|---|
| Server signing key (private) | License server only, mode 0400 | Ed25519 PEM |
| Server signing key (public) | Compiled into the desktop binary | base64 in src-tauri/src/licensing.rs |
| Activation token | ~/.config/quay/license.json (per OS) | base64url-payload + signature |
| Licence key | license.json next to the activation token | mode 0600, plaintext |
The licence-server private key never leaves the production VPS; losing it is the only thing that would force every device to re-activate. (We’ve kept the same key since v0.1.)