Skip to content

Offline leases

A lease is a short signed document Keylight hands the SDK at every successful activate or validate call. The app keeps it in the Keychain and re-verifies it on every launch - no network required.

This is what makes Keylight offline-first: a customer can board a plane, launch your app at 30,000 feet, and stay licensed.

{
"kid": "primary",
"licenseKeyHash": "4e7a...",
"instanceId": "5f7e7b9a-...",
"issuedAt": 1713546000,
"expiresAt": 1714150800,
"status": "active",
"signature": "MEQCIB..."
}

Bound to:

  • kid - which of your app’s Ed25519 keys signed it.
  • licenseKeyHash - SHA-256 of the normalized license key. The SDK rejects leases that don’t match the stored license.
  • instanceId - the specific activation slot. Leases from other devices don’t verify here.
  • expiresAt - 7 days after issuedAt. After that, the SDK must refresh.

Full wire format and signing rules: Ed25519 leases.

Two limits apply, whichever fires first:

  • Lease lifetime: 7 days from the moment Keylight signs. After that, the SDK must refresh.
  • maxOfflineDays: 15 days by default since the last successful online activate or validate. The SDK records lastValidatedOnline in the Keychain on every successful network call and rejects cached leases past this window — even if the lease itself hasn’t expired. Tune it on KeylightConfiguration if your app needs a stricter or looser cap.

The SDK refreshes eagerly inside the last 24 hours of the lease (see License lifecycle), so normal users re-sign well before either limit. A user who activates and then goes fully offline gets at minimum 7 days from lease issuance, and at most 15 days from the last online check.

If the lease expires (or the offline cap is hit) while offline:

  • The app flips to .expired on next launch.
  • It stays there until the next successful /validate refresh, which will either restore .licensed or confirm the expiry.

maxOfflineDays is cleared from the Keychain on deactivate, so a re-activated install starts fresh.

What the SDK checks before trusting a lease

Section titled “What the SDK checks before trusting a lease”

On every launch:

  1. Signature. Ed25519-verified against the kid-indexed public key embedded in the app build.
  2. Unknown kid → reject. If the lease’s kid isn’t in the app’s trusted-keys map, the lease is dropped. This is how key rotation and compromise-response work.
  3. License-key hash match. The lease’s licenseKeyHash must equal SHA-256 of the currently-stored key.
  4. Instance ID match. The lease’s instanceId must match the instance ID the SDK is using for this device.
  5. Clock manipulation check. If the system clock has moved backward by more than 1 hour since lastSeen, the lease is treated as invalid. Details in Ed25519 leases.
  6. Expiry. expiresAt compared against now.

Any failed check → the SDK falls back to a network refresh. If that also fails, state goes .invalid.

7 days is a deliberate balance:

  • Short enough that a revoked license stops working within a week everywhere, even for users who haven’t refreshed.
  • Long enough that intermittent connectivity (a bad hotel wifi week, a flight, a server outage) doesn’t interrupt legitimate paying users.

The lease TTL is fixed platform-wide - you can’t tune it per account.

Keylight deliberately has no remote revocation of leases already in the field. Revoking a license records the revocation server-side and blocks future activations - but an existing lease continues to verify until it expires.

This is a design tradeoff: remote revocation would break offline verification. The 7-day lease TTL is the revocation window.

If you need to kill a compromised key immediately and the leased instance is still using it, you can:

  1. Revoke the license in the dashboard - new activations blocked, next refresh returns invalid.
  2. Ask the user to launch the app while online; the next validate call will deliver the revocation.
  3. For mass compromise (signing key leak), rotate the signing key and ship a new app build that removes the old kid. Details in Key rotation.