Skip to content

Offline activation

Offline activation lets your app validate a license key locally, without calling the Lizen API. Useful for air-gapped environments, unreliable connections, or ultra-low latency requirements.

Available on Hobby tier and above.

How it works

  1. Your app calls POST /v1/offline/generate (once, while online). Lizen returns a signed license file — an RS256 JWT — tied to the specific device fingerprint.
  2. Your app stores the license file locally.
  3. On subsequent startups, your app verifies the JWT signature locally using the embedded public key. No network call needed.
  4. Every 90 days (or at the license’s expires_at, whichever is sooner), the app must sync online to get a fresh license file and pick up any revocations.

Security model

  • License files are RS256-signed JWTs. The private key lives in Cloudflare Worker Secrets and never leaves the server.
  • Each license file is bound to a specific device fingerprint. It cannot be copied to another device.
  • Revocations are picked up at the next sync. Maximum offline window before revocation takes effect: 90 days.
  • Perpetual licenses (no expires_at) still expire after 90 days offline — this forces phone-home to pick up revocations.

JWT payload

{
"iss": "https://api.lizen.dev",
"sub": "lic_abc123",
"iat": 1755648000,
"exp": 1763424000,
"kvl": {
"plan": "pro",
"deviceFingerprint": "sha256_of_device_id",
"activationLimit": 3,
"keyValue": "MYAPP-ABCD-EFGH-IJKL"
}
}

Generate a license file

const license = await lizen.offline.generate({
key: 'MYAPP-ABCD-EFGH-IJKL',
deviceFingerprint: getMachineId(),
deviceName: "Alex's MacBook Pro", // optional
});
// Save locally
await fs.writeFile('license.jwt', license.licenseFile);

Verify locally (no SDK needed)

import * as jose from 'jose'; // or any JWT library
const publicKey = await jose.importSPKI(embeddedPublicKey, 'RS256');
const { payload } = await jose.jwtVerify(licenseFileContent, publicKey);
if (payload.kvl.deviceFingerprint !== getMachineId()) {
// License file is for a different device
throw new Error('Device mismatch');
}

Sync (extend the offline window)

Call POST /v1/offline/sync periodically to get a fresh license file with a new expiry. The device must have been previously activated.

const refreshed = await lizen.offline.sync({
key: 'MYAPP-ABCD-EFGH-IJKL',
deviceFingerprint: getMachineId(),
});
// Save updated license file
await fs.writeFile('license.jwt', refreshed.licenseFile);

Sync also picks up revocations — if the key has been revoked, sync returns a 422.