Skip to content

.NET SDK — offline, freshness and grace

The SDK does not call the server on every check. After activation it holds a signed, cached license and validates it locally against the embedded provider key. This is what lets you gate features on the hot path without a network round trip — and what lets a short connectivity blip pass unnoticed.

The cached license lives on disk in a LicenseFileStore, keyed to the device fingerprint, in a default location under application data. On startup the SDK serves the cached license first and validates its signature against the pinned provider key, so the application starts licensed without waiting on the network. On the fluent-builder path, UseLicenseStoreBuilder() enables the cache and lets you move it:

.UseLicenseStoreBuilder(store => store
.UseLicenseFolderLocation(LicenseFileStore.LicenseFolderLocation.UserDefined, path))

The cached license is encrypted on disk, on by default. This closes a gap the signature alone doesn’t: a signed license proves it wasn’t forged, but a plain JSON file gives up its contents to anything that can read it — a backup, a synced folder, malware scraping known paths.

The encryption key derives from three inputs: your application’s key material (a built-in default, or your own via UseProtectionKey), a random salt generated per file, and the device’s node id. Binding to the device is what matters most: copy the cache to another machine and it won’t decrypt there — and the signature still rejects a forged one.

.UseLicenseStoreBuilder(store => store
.UseProtectionKey(myAppEmbeddedKeyBytes)) // optional — a built-in default is used otherwise

Decryption failure — wrong key, a different device, a tampered file — is always treated as a cache miss, never a license denial: the SDK falls back to revalidating online rather than locking a legitimate user out because a file didn’t decrypt. A cache written by an SDK version predating this feature is read once as plaintext and silently rewritten encrypted.

Call DisableCacheProtection() on the store builder if the cache file needs to stay human-readable (debugging, an environment where you handle protection yourself); the license signature and device binding are unaffected either way.

Each cached license carries a lease describing how recently it was authoritatively validated. LeaseStatus captures where it stands:

Status Meaning
Fresh Validated within the normal freshness window. Used directly.
Stale Past freshness but still inside the allowed offline window — should revalidate soon.
StaleLockout Past the absolute offline limit — must revalidate before further protected use.
Unverifiable Freshness can’t be determined; refused like StaleLockout — the license must be revalidated before use.
Lease freshness — how recently the license was revalidated time → Fresh used directly Stale revalidate soon StaleLockout must revalidate freshness TTL absolute offline limit License validity — grace keeps features on after expiry time → Active IsOperational = true GracePeriod still true — renew now Expired IsOperational = false license expires grace window ends

The background refresh (every LicenseRefreshIntervalSeconds) keeps the lease Fresh in normal operation. A backend outage shorter than the allowed offline window has no user-visible effect: the lease may go Stale, but the license keeps working until the absolute offline limit. A prolonged outage eventually moves the lease to lockout, at which point the license must be revalidated before protected features resume — the SDK will not extend an unverified license indefinitely.

Grace is separate from freshness: it’s the window after a license’s validity ends during which the product keeps working, giving a customer time to renew. While in grace, LicenseStatus is GracePeriod and IsOperational stays true, so your existing feature checks keep passing without special casing. Read LicenseStatus == LicenseStatus.GracePeriod if you want to show a “renew now” banner.

Offline enforcement can’t trust the system clock alone — moving it backwards would revive an expired license. The SDK keeps a monotonic high-water record of observed time, so winding the clock back doesn’t restore expired access. This is automatic; you don’t configure it.

For air-gapped or intermittently connected installs, add the Revenusion.MonetizeIt.Client.OfflineStore package. It lets you carry activation across an air gap: the device produces a signed request, an operator activates it against the platform, and the device imports the signed response — all verified against the same provider key, so an offline device is no less protected than an online one.