Open-source C# SDK for Keylight — license your .NET, Godot, and Unity apps with online activation and offline Ed25519 license verification.
In one line: a software-licensing SDK for C# — license-key activation and validation, entitlement/feature gating, trials, and tamper-resistant offline license verification (signed
v3lease, Ed25519 + clock-skew tolerance) for .NET apps, Godot 4 (via NuGet), and Unity (UPM package coming). Async-native, dependency-light, and fully nullable-annotated.
Licensing shouldn't mean bolting a heavyweight, phone-home-or-die SDK onto your app.
- Works offline. The license is a signed lease your app verifies locally with Ed25519 — no network round-trip to gate a feature, no lockout when the machine is offline.
- Tamper-resistant by design. Entitlements live inside the signature; a forged or hand-edited lease can't pass verification without the tenant's private key.
- Async-native, no surprises. Every network operation is a
Task-returning async method withCancellationTokensupport.StateandHasEntitlementare synchronous reads from the in-memory cache — no deadlocks. - One SDK family. Verifies licenses identically to the Swift, Rust, and JavaScript SDKs, proven by shared conformance vectors.
- Why Keylight
- Features
- Runtime Support
- Quick Start
- License Lifecycle
- License States
- Entitlements
- Offline Validation
- Refresh and Trials
- Configuration Reference
- Godot and Unity
- Conformance
- Documentation
- Other SDKs
- License
- License Lifecycle — Activate, validate, and deactivate license keys with a small, explicit API.
- Offline Verification — The single offline artifact is a signed
v3lease, verified with Ed25519 and a 300-second clock-skew tolerance. An optionalMaxOfflineDaysgrace caps how long a device may run without checking in. - Async-native — All network operations are
Task-returning withCancellationTokensupport. Synchronous wrappers (Activate,Validate,Deactivate) are provided for non-async contexts. - Synchronous reads —
StateandHasEntitlementare synchronous, backed by the in-memory lease cache — no deadlocks from sync-over-async. - Entitlements — Feature gating from the cached lease:
client.HasEntitlement("pro"). - Trials — Built-in local trial timer, auto-started on first launch via
CheckOnLaunchAsync. - Device Telemetry — Auto-attaches
sdk_version,platform, and (optional)app_versionon every API call. - Network Resilience — Validate network failures are non-fatal (the client retains whatever state the cached lease dictates).
- Pluggable — Swap the storage backend (
ILeaseStore) or HTTP transport (IKeylightTransport) via interfaces for tests or custom platforms. - Nullable-annotated — Targets
netstandard2.0andnet8.0; ships with#nullable enablethroughout and an.snupkgsymbol package.
Targets netstandard2.0 (broad compatibility: .NET Framework 4.6.1+, .NET Core 2.0+, Mono,
Godot 4 via NuGet) and net8.0. The core library has zero runtime dependencies — pure
managed code with no NuGet references (important for IL2CPP/WebGL targets).
A Unity UPM package (dev.keylight.sdk) is available — see Godot and Unity.
dotnet add package Keylightusing System.Net.Http;
using Keylight;
// Fetch the tenant's trusted Ed25519 keyset so leases can be verified offline.
// (You can also pin keys explicitly via .TrustedKeys(...) on the builder.)
var http = new HttpClient();
var keyset = await Keyset.FetchAsync(http, "https://api.keylight.dev", "your-tenant");
var config = KeylightConfig
.Builder("your-tenant", "your-product", "sdk_live_…")
.TrustedKeys(keyset?.Keys ?? new Dictionary<string, string>())
.MaxOfflineDays(15) // optional offline grace window (15 is the default)
.Build();
var client = new KeylightClient(config);
// Activate a license key (online). The returned lease is Ed25519-verified
// *before* anything is persisted.
await client.ActivateAsync("USER-LICENSE-KEY");
// Gate features on entitlements — synchronous, from the cached lease.
if (client.HasEntitlement("pro"))
{
// unlock pro features
}
// Release the seat when uninstalling / switching devices.
await client.DeactivateAsync();Call
await client.CheckOnLaunchAsync()on startup to refresh the lease if it is stale, and to auto-start the trial clock on first launch whenTrialDurationDaysis configured.
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ ActivateAsync │────▶│ ValidateAsync │────▶│ DeactivateAsync │
└─────────────────┘ └──────────────────┘ └──────────────────┘
▲
│ on launch / on events (no background timers)
┌──────────────────────────┐
│ RefreshIfNeededAsync │
└──────────────────────────┘
| Method | Description |
|---|---|
ActivateAsync(key) |
Activates a key on this device. Verifies the returned lease before persisting; throws ActivationException on server error and LeaseVerificationFailedException on a bad signature. |
ValidateAsync() |
Re-checks the stored license online. Updates the cache if the server returns a new lease; network failures are non-fatal. |
DeactivateAsync() |
Releases the seat and clears local license state, even if the network call fails. Call on uninstall or device switch. |
RefreshIfNeededAsync() |
Validates only if due (debounce 5 min, stale 6 h, or within 24 h of expiry). Safe to call often. |
CheckOnLaunchAsync() |
Convenience: refresh if a license is stored; also auto-starts the trial clock on first launch. |
ActiveRevalidateAsync() |
Forces a validate on active use (foreground / popover / focus), debounced 60 s in memory. Bypasses the staleness gates so a revoke lands mid-session instead of at the next launch. Never throws; a transient failure never downgrades a live session. |
Synchronous wrappers Activate(key), Validate(), and Deactivate() are provided for callers
that cannot use async/await (every await in the async path uses ConfigureAwait(false)).
client.State resolves a single high-level status from the cached, signature-verified lease
(no network call). It is a KeylightState enum:
| State | Meaning |
|---|---|
Licensed |
Current, signature-valid active lease. |
Trial |
No license, but a local trial is active. |
Expired |
Lease expired, or a previously stored license is no longer current. Also mapped from lease status: "fallback" (cross-SDK note: Swift and Rust surface a distinct Limited state; C# maps it to Expired). |
Invalid |
No valid lease and no trial in progress. |
switch (client.State)
{
case KeylightState.Licensed:
// full access
break;
case KeylightState.Trial:
// trial UI
break;
case KeylightState.Expired:
case KeylightState.Invalid:
// prompt to activate
break;
}Entitlements are feature keys carried inside the signed lease and checked offline:
if (client.HasEntitlement("cloud-sync"))
{
EnableCloudSync();
}HasEntitlement returns true only when the cached lease is signature-valid, unexpired, and not
expired-status — so offline feature gating never disagrees with the resolved Expired state.
When MaxOfflineDays is set, it also gates on the offline grace window.
The offline artifact is a signed v3 lease issued by the Keylight API. The SDK reconstructs
the exact signed payload (entitlements sorted, pipe-delimited) and verifies it with Ed25519
against the tenant's trusted keyset, applying a 300-second clock-skew tolerance.
// Pin trusted keys explicitly instead of fetching them:
var config = KeylightConfig
.Builder("your-tenant", "your-product", "sdk_live_…")
.TrustedKeys(new Dictionary<string, string>
{
{ "k1", "<raw Ed25519 public key, base64>" }
})
.MaxOfflineDays(15) // default; omit to run offline as long as the lease itself is current
.Build();- The trusted keyset can be fetched once with
Keyset.FetchAsync(http, baseUrl, tenantId)or pinned at build time via.TrustedKeys(...). client.Stateandclient.HasEntitlementread from the in-memory verified-lease cache — no network call.
There are no background timers. The host drives refresh on launch and on meaningful events:
await client.CheckOnLaunchAsync(); // validate if due + auto-start trial clock
await client.RefreshIfNeededAsync(); // call again on window-focus / purchase / resume
await client.ActiveRevalidateAsync(); // app came forward: force a check (60 s debounce)RefreshIfNeededAsync is the cheap, often-called path — it skips the server when the cache is
fresh. ActiveRevalidateAsync is the prompt one: it always talks to the server (debounced to
60 s) so a dashboard revoke takes effect within minutes of the user touching the app rather than
waiting for the lease to expire or the app to relaunch. Wire it to whatever "the user is here
now" signal your host has — app activation, window focus, menu-bar popover opening.
Trials are local and offline-first. Set TrialDurationDays on the builder, then call
CheckOnLaunchAsync — the trial clock is started automatically on the first launch when no trusted
active license is present:
var config = KeylightConfig
.Builder("your-tenant", "your-product", "sdk_live_…")
.TrialDurationDays(14)
.Build();
var client = new KeylightClient(config);
await client.CheckOnLaunchAsync(); // starts the trial on first launch
if (client.State == KeylightState.Trial)
{
ShowTrialBanner();
}Built with KeylightConfig.Builder(tenantId, productId, sdkKey):
| Builder method | Type | Default | Description |
|---|---|---|---|
(required) Builder(tenantId, productId, sdkKey) |
string |
— | Your Keylight tenant, product, and SDK key. All three are required. |
.TrustedKeys(dict) |
IDictionary<string,string> |
empty | Trusted Ed25519 public keys (kid → base64) for offline verification. |
.MaxOfflineDays(n) |
int |
15 |
Offline grace window since last online validation. Set 0 to run offline as long as the lease itself is current. |
.TrialDurationDays(n) |
int |
— | Local trial length in days. Omit to disable trials. |
.AppVersion(v) |
string |
— | Reported in activation/validation telemetry. |
.KeyPrefix(p) |
string |
— | Client-side key-format check (e.g. "PROD"). |
.BaseUrl(url) |
string |
https://api.keylight.dev |
API base URL. |
Godot 4 — install via the standard NuGet workflow. Add Keylight to your project's
.csproj or use the Godot editor's NuGet integration. The netstandard2.0 target is compatible
with Godot's .NET 6+ Mono runtime.
Unity — a UPM (Unity Package Manager) package (dev.keylight.sdk) is included in this repo
under unity/dev.keylight.sdk. Install it via Unity's Package Manager (UPM) using the Git URL, or
via OpenUPM once published. The package source is kept in sync with
src/Keylight via unity/sync-core.sh.
The security-critical lease verifier is gated by Keylight's frozen cross-SDK conformance vectors
(tests/Keylight.Tests/ConformanceTests.cs). The C# verifier must agree with every vector on
{ KidKnown, SignatureValid, Expired }, which keeps offline verification behavior identical across
the Keylight SDK family (Swift, Rust, JavaScript, C#, …).
dotnet test tests/Keylight.Tests- Platform docs: docs.keylight.dev
- Website: keylight.dev
- API host:
https://api.keylight.dev
| Platform | Status | Repository |
|---|---|---|
| Swift (macOS/iOS) | Available | keylight-swift |
| Rust (CLIs/daemons/Tauri) | Available | keylight-rust |
| JavaScript/TypeScript | Available | keylight-js |
| C# (this repo) | Available | keylight-csharp |
| Unity (UPM) | Available | keylight-csharp — unity/dev.keylight.sdk |
| C++ | Planned | unified by the same cross-SDK conformance vectors |
Keylight is the licensing layer for desktop apps. You keep your own Stripe account, your own pricing, and your own customers — Keylight issues the licenses and tells your app who is allowed to run it.
- License keys issued automatically when a payment completes
- Device activations with limits you set, and self-serve deactivation
- Offline validation — signed Ed25519 leases your app verifies locally
- Feature entitlements signed into the lease, so tiers work offline too
keylight.dev · Documentation · Pricing
- Licensing a Cross-Platform App from One Control Plane
- License Your Unreal Engine Game Offline in an Afternoon
- One-Time vs Subscription Licensing: Which to Use?
MIT License. See LICENSE for details.
Keylight C# SDK — software licensing for .NET: license-key activation & validation, offline Ed25519 lease verification, entitlement/feature gating, trials, and pluggable storage/transport — for .NET, Godot 4 (NuGet), and Unity (UPM coming).