Skip to content

Licence + activation

Three pieces: licence keys, activation tokens, and the device list. They cover three different “who-is-this” questions.

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.

Pasting the licence key into Settings → Licence runs:

  1. App POSTs /v1/activate to 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>)
  2. 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
  3. 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.

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.

TierDevices
Freeunlimited (no enforcement)
Pro2
Pro Plus5

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 portal

The app surfaces this clearly with a one-click link to the portal.

If a licence is revoked (refund processed, abuse, etc.):

  • Server-side: licenses.status flips to revoked. 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).
WhatWhereFormat
Server signing key (private)License server only, mode 0400Ed25519 PEM
Server signing key (public)Compiled into the desktop binarybase64 in src-tauri/src/licensing.rs
Activation token~/.config/quay/license.json (per OS)base64url-payload + signature
Licence keylicense.json next to the activation tokenmode 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.)