If you ship a desktop app outside an app store, you eventually hit the same wall: how do you check a license when the user is on a plane, behind a corporate firewall, or just offline? Calling your server on every launch isn't an option. Here's how offline activation actually works, without the hand-waving.
The naive version, and why it breaks
The first thing everyone reaches for is "call home on launch, get back yes/no." It works in the demo and fails in the wild:
- No network = no app. Fail-closed locks out paying customers. Fail-open means anyone who blocks your domain runs free. Both are bad.
-
A boolean is forgeable. If your app trusts a
{"valid": true}response, a proxy or a patched DNS entry returns that for free.
The fix isn't a better endpoint. It's moving the trust off the network and onto cryptography.
The model that works: signed leases
The durable pattern is a cryptographically signed lease (Keygen calls these license files, Keylight calls them leases — same idea):
- On first activation, the device talks to the server once.
- The server returns a small signed document: the license state, an expiry, the device binding, and any entitlements (which features/tiers are unlocked).
- The document is signed with the server's private key (Ed25519 is the modern choice — small, fast, boring in the good way).
- Your app ships the matching public key and verifies the signature locally on every launch. No network needed.
Because the app only ever verifies with a public key, there's nothing secret in the binary to steal, and a forged lease fails the signature check. That's the whole trick: the server vouches once, math vouches forever after.
first launch ──► server signs lease (Ed25519, private key) ──► stored on device
every launch ──► app verifies signature (public key) ──► no network
Device binding (so one key isn't infinite installs)
A lease is bound to a device so a single license can't be pasted onto a thousand machines. The lease embeds a device fingerprint, and the SDK checks the running machine matches. The honest engineering note: fingerprints drift. macOS hardware UUIDs are stable; "hostname + user" is not. Pick a stable identifier and give users a deactivate path, or you'll drown in "I reinstalled and now I'm locked out" tickets.
The tradeoff nobody mentions: revocation vs. offline
Here's the tension you have to design around deliberately. A purely offline lease can't be revoked instantly — that's the point, it doesn't phone home. So a refunded or charged-back user keeps a valid lease until it expires.
You resolve it with a max-offline window. The lease carries an expiry (say 7, 14, 30 days). Inside the window, fully offline. Past it, the app must revalidate online once to refresh the lease — which is your chance to revoke. Short window = tighter control, more online checks. Long window = friendlier offline story, slower revocation. There's no universally right number; it depends on your price point and abuse surface.
What this looks like with an SDK
You don't want to hand-roll Ed25519 and lease parsing. Most licensing SDKs hide this behind a couple of calls. With Keylight, for example, the offline path collapses to: activate once, then a local checkOnLaunch() that verifies the lease and hands you a state — licensed, trial, expired, invalid — with no network call. Entitlements ride inside the signed lease, so feature gating is offline too. Keygen, Cryptolens, and LicenseSpring implement the same primitive with different ergonomics; the underlying cryptography is the part that matters and it's the same everywhere.
The takeaway
Offline activation isn't "cache the server's answer." It's: trust the network once, trust signatures forever after, bind to a stable device id, and pick a max-offline window that matches how fast you need to revoke. Get those four right and your app works on a plane and still says no to a refunded license.
If you're building this for a Mac or Tauri/Electron app and don't want to implement the crypto yourself, I wrote up the Keylight offline verification model here.
Top comments (0)