To add a free trial that doesn't leak, issue a signed, time-limited license key that auto-expires, node-lock it so one machine gets one trial, keep it revocable and tracked, and let it convert to paid without re-keying — the opposite of a registry flag or a date file, both of which a user resets in seconds. This post covers what "done right" means, why the naive approaches fail, and how to wire a self-service trial button on your own site so you never email a license by hand.
Why the easy trial always leaks
The tempting way to build a trial is to record when the software first ran and count down from there. Two variants, both broken:
- The registry / date-file trial. On first launch you write a timestamp to a registry value or a file, and on later launches you compare against it. Deleting that value — or reinstalling into a fresh VM — resets the clock. There's nothing signed, so the app has no trustworthy memory of whether the trial was already used.
-
The clock-comparison trial. You store an expiry and compare it against
DateTime.UtcNow. Roll the system clock backward and the trial never ends. Without a signed reference time, the app can't tell a real date from a rolled-back one.
Both share the same root flaw: the trial state lives somewhere the user fully controls, in a form nothing can verify. The fix isn't a cleverer hiding spot — it's a signed key the app can actually trust.
What a leak-resistant trial looks like
A proper trial is just a license with a short life. It carries the same protections a paid license does, so the same cheats don't work.
It's a signed, auto-expiring key
The trial key is a payload (product, tier, seats, expiryUtc, a trial flag) signed server-side with your private key. Your app verifies the signature against an embedded public key and reads the expiry from the signed payload — not from a value on disk the user can edit. When the expiry passes, the key stops validating and the app fails closed to the free/unlicensed state. No server call is needed for it to expire; the expiry is baked in and tamper-evident.
Because it's a real signed license, the clock-rollback cheat is covered too. Keyright's SDK remembers the latest time it legitimately saw and treats a large backward jump on a time-limited license as tampering — a ClockTampered status that refuses to validate until the clock is corrected (see the .NET SDK integration guide).
It's node-locked — one trial per machine
A signed key with an expiry is still copyable, so bind it to the machine. Node-locking ties the trial to a stable machine fingerprint (with a small swap tolerance so a hardware change doesn't lock an honest evaluator out), which means one device gets one trial rather than one key seeding a hundred installs.
It's issued one-per-identity
Node-locking stops one key spreading; issuing controls stop one person taking endless new trials. Keyright's self-service trial is one trial per email per product — re-requesting with the same email returns the same key (created: false) instead of minting a new one. A page refresh or a second click never resets the clock or stacks trials. It's idempotent by design, which is also what makes it abuse-resistant.
It's revocable and tracked
A trial you can't kill is a liability. Keyright trial keys are fully revocable like any license, and every trial is tracked in your dashboard — the Home overview counts them, and the Licenses table shows each one with its expiry and per-machine activations. You can see who's trialing, when their key lapses, and reach out before it does.
It converts to paid without re-keying
The single most important property: a trial should become a purchase with no friction. Because a Keyright trial key activates through the exact same path as a paid key, when the customer buys and activates a non-trial key it supersedes the leftover trial with no reinstall and no re-keying. The app flips from "Pro Trial — 27 days left" to the paid edition on the next activation. Your app reads the state straight off LicenseInfo — info.IsTrial, info.DaysRemaining, info.StatusBadge — so the trial countdown and the upgrade prompt are a few property reads, not a separate code path.
Self-service: a trial button on your own site
The best trial funnel doesn't involve you at all. Keyright's trial endpoint, POST /v1/trial, is public and CORS-open, so a plain form on your marketing site can call it directly and the customer gets a key by email — no dashboard clicks, no hand-sent license files.
<button id="trial">Start free trial</button>
<script>
document.getElementById('trial').onclick = async () => {
const res = await fetch('https://keyright.delta1labs.com/v1/trial', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ product: 'acme-app', email: userEmail, company: userCompany }),
});
const data = await res.json();
if (res.ok) show(`Your trial key: ${data.key} (expires ${data.expiresUtc})`);
else show(data.error);
};
</script>
Trials are off by default — a product offers one only once you opt in by setting a trial length, tier, and seat count (in the dashboard's Products tab, or POST /admin/products/{slug}/trial). The response returns the key, its tier, and expiresUtc, and Keyright emails it to the address. The full flow, field reference, and status codes are in the self-service free trials guide.
For an air-gapped or enterprise evaluator that can't reach the network, you can hand out a signed, time-limited offline trial file instead — same trial semantics, delivered as a file the SDK verifies locally against your public key.
The customer activates it like any key
There's no separate "trial mode" in your app. The trial key activates through the same ActivateAsync(key) call as a paid key: it performs an online activation, binds the machine, and caches a signed lease with trial: true and the trial's expiry. Thanks to that cached lease the app keeps working offline until the trial ends, then fails closed. One code path handles trial and paid alike.
Be honest about the ceiling
A signed, node-locked, tracked trial defeats the casual cheats — deleting a registry key, rolling the clock, refreshing for a new trial — comprehensively. What it can't do alone is stop a determined attacker who decompiles your app and patches the check out, because that check runs on the user's machine. That's the same limit every client-side protection has, and the same answer applies: pair the trial's client check with online activation, so expiry, seats, and revocation are decided on a server the attacker doesn't control, and with obfuscation so patching the client is expensive. The trial gate is honest and hard to reset; the server is where it's enforced.
Add a trial this week
A trial done right is a signed, expiring, node-locked, revocable key that converts to paid on its own — and Keyright gives you all of it, self-service, from one API call. Turn on trials for your product, drop a button on your site, and read the state off LicenseInfo in your app. Sign up free and follow the self-service free trials guide to wire it end to end — the free plan is enough to ship a real, leak-resistant trial before you pay anything.
Top comments (0)