minmaxkey

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 validate call → fresh token cached,
  • revoked/expired: validate says no → deny.