Offline tokens
Offline tokens
Offline tokens are what let your app keep working without a network call on every launch — and what makes customers not hate you during your server's occasional hiccup.
What they are
An Ed25519-signed JWT, produced by your server (the product's private key
never leaves it) and verified locally with the product's public_key embedded
in your app.
Claims:
| claim | meaning |
|---|---|
iss |
always minmaxkey |
sub |
the license key |
product |
product slug |
policy |
{"kind": ..., "max_seats": ...} |
fingerprint |
machine fingerprint the token is bound to ("" = unbound) |
exp |
present only for subscriptions/trials |
iat / jti |
issued-at timestamp / unique id |
A lifetime license has no exp — it's valid until you revoke it.
The trade-off you're signing up for
Offline tokens are verified without the server, so:
- revocation is not instant. A revoked license keeps working offline until
the client revalidates. Practical countermeasures: check periodically (the
SDK's
ensure_valid()does exactly this) and require online validation for features that cost you money (cloud sync, updates). - the public key is in your binary. The public key alone mints nothing — signatures still come from your server. But anyone can extract the key and study your client. Offline licensing is about stopping casual key-sharing, not nation-state pirates. If your app is pure client-side, that ceiling is physics.
Verifying
Any language can do this with standard Ed25519 primitives. The Python SDK has
it built in; the reference implementation is scripts/verify_license.py
(cryptography-only, no server, no other deps):
python scripts/verify_license.py product.pub.pem "<token>" \
--fingerprint 9f86d081884c7d659a2feaa0c55ad015
# VALID (exit 0)
# INVALID ... (exit 1)
Checks performed: algorithm must be EdDSA, issuer must be minmaxkey,
signature must verify against the product public key, exp must not have
passed (with 60s clock skew), and the optional fingerprint must match.
Keeping tokens fresh
POST /v1/products/{id}/validate?key=...&fingerprint=... returns a fresh
signed token whenever the license is valid. The SDK caches the last good
token and swaps it out on revalidation — that's the full
auto-revalidation story:
- offline: cached token valid → instant approval, zero network,
- online: cache invalid → one
validatecall → fresh token cached, - revoked/expired:
validatesays no → deny.