All guides

How to Add a Free Trial to Your Desktop App Without Easy Trial Resets

Build a 14-day software free trial license with server-side expiry, device binding, honest offline limits, and a paid upgrade path that does not require reinstalling.

A customer installs your desktop app, tries it for fourteen days, then reinstalls it. If that gives them another fourteen days, you have not built a trial policy: you have built a local timer. A software free trial license moves the important decision off that computer. The server remembers when access expires; the app checks a time-limited key rather than inventing a new start date on every installation.

This guide is for indie developers shipping desktop applications, plugins, and independently distributed Unity tools. The goal is to prevent easy trial resets without making legitimate users reinstall, lose projects, or contact support when they decide to buy. It is not a promise of uncrackable desktop DRM.

Software free trial license: the workflow

From evaluation to a paid license, in the same installation
  1. Trial issuedServer sets the 14-day expiry
  2. Device activatedApp checks the key and device
  3. Purchase confirmedServer verifies the payment
  4. Paid key issuedCustomer activates it in the same app

Choose a time-limited trial, a feature-limited trial, or both

A time-limited trial gives users a deadline: for example, fourteen days to evaluate the full product. A feature-limited trial lets them explore the workflow but reserves specific capabilities, such as batch export, for a paid license. You can combine the two, but make the limits visible before a user invests an afternoon in a project.

  • Time-limited: suitable when a complete workflow is the value users need to experience. Decide whether the clock starts at issuance or first activation.
  • Feature-limited: suitable when users can meaningfully evaluate the tool without its paid capabilities. Enforce entitlements at the operation itself, not only by hiding a menu.
  • Combined: useful for expensive export or integration features, provided the offer is still easy to explain.

In PermitCore, a trial key has IsTrial, TrialDays, and a server-side ExpiresAt. The standard trial expires from creation, not first launch. Feature flags are a separate control; setting the trial flag does not automatically disable exports. See trial license configuration and feature-based licensing.

Why local timers are easy to reset

A first-run timestamp in a file, registry value, preference, or local database belongs to an environment the user controls. Reinstalling may remove it. Deleting application data may make the next launch look like the first. Restoring a virtual-machine snapshot can restore the entire earlier state. Comparing that timestamp to the system clock makes rolling the clock backward another possible bypass.

Encrypting the local timestamp can discourage casual editing, but it does not stop someone deleting or restoring the whole record. A generated installation UUID has the same weakness if it disappears during uninstall.

The practical answer to how to add a trial to a desktop application is to separate two questions: is this issued key still valid? and is this person or device eligible for another trial? A license server answers the first. Your trial-issuance policy must answer the second.

Device binding is not the same as one trial per device

For a one-device evaluation, issue a node-locked trial with one activation. Activate it with a stable device ID, and use the same ID when the app checks that device again. Removing a local file does not change the existing key's server-side expiry or erase its activation history.

But a different trial key starts a different license record. Node locking does not, by itself, prevent a user from obtaining a fresh key under another account. If your requirement is prevent trial reset desktop app, you also need an issuance record that survives reinstalling.

  1. Your authenticated trial endpoint receives a product identity and a device identifier.
  2. Your backend checks its product-and-device eligibility record and any account restrictions.
  3. It atomically reserves eligibility and issues or returns the existing trial. Concurrent requests must not produce two fresh trials.
  4. It retains the original expiry, even when the app is reinstalled or the user requests the key again.

This eligibility endpoint is application-specific backend work, not a built-in PermitCore SDK method. PermitCore Store currently guards trial claims by email plus product and adds a per-IP/product 30-day reservation. Its claim request does not implement a global one-trial-per-device ledger. IP limits are only an additional signal: shared offices and changing networks make an IP a poor substitute for a device or customer identity.

Choose identifiers deliberately. The SDK defaults are convenient, but a file-seeded ID can change if its seed is deleted; do not assume it survives an uninstall. Hardware identifiers can also change after repairs or OS changes, and a modified client can spoof what it submits. Hashing an identifier does not make it spoof-proof. Provide a documented recovery path rather than silently granting another fourteen days. Our device activation guide, hardware ID pitfalls, and key-sharing controls explain the tradeoffs.

Configure a 14-day trial on the server

In Licenses → New License, choose the product, enable Trial License, and set the duration to 14 days. Enable node locking, allow one activation, and choose the trial feature flags your app will enforce. Keep post-expiry grace unset or zero if fourteen days must be the online access deadline. For a strict online evaluation, also disable offline grace.

Alternatively, configure a Store product's trial duration and license settings, then let the signed-in customer claim it. Trial claims do not require Stripe payment. The client examples below consume an already-issued time-limited trial license key; they do not create a trial or calculate its authoritative expiry. Never ship a CompanyAdmin API key or license-generation credentials inside a desktop binary.

The examples intentionally use online-first checks with offline caching disabled. They call activation on each launch so the request includes the device ID: PermitCore's plain validation call does not carry a device ID. A successful same-device reactivation of a node-locked key does not consume another seat. Pass the expected product ID as well, so a key for another product cannot unlock this one.

.NET: check the issued trial at startup

Install PermitCore.Client. Call this method with the saved trial key, your actual product GUID, and the stable device ID chosen by your application. Invoke it again when a customer supplies a paid replacement key.

using PermitCore;

async Task<LicenseResult> CheckLicenseAsync(
    string key, Guid productId, string deviceId)
{
    using var client = new PermitCoreClient(new PermitCoreOptions
    {
        BaseUrl = "https://api.permitcore.dev",
        EnableOfflineCache = false
    });
    return await client.ActivateAsync(
        key, deviceId: deviceId,
        deviceName: Environment.MachineName,
        expectedProductId: productId);
}

Only enable the protected workflow when the returned result is valid. Use IsTrial and TrialDaysRemaining to display evaluation status, and HasFeature("export") for an export entitlement you actually configured. On failure, preserve the user's work and offer reconnect, enter paid key, or purchase actions; do not generate another trial automatically. See the full .NET tutorial.

Electron: keep the check in the main process

Install @permitcore/permitcore. This TypeScript helper runs in Electron's main process; pass the saved key, product ID, and device ID from your application. Keep the enforcement decision there and expose only a narrow result through your preload bridge. Do not turn on Node integration in the renderer to make licensing easier. Follow Electron's security checklist.

import { app } from 'electron';
import { PermitCoreClient } from '@permitcore/permitcore';

const client = new PermitCoreClient({
  baseUrl: 'https://api.permitcore.dev',
  enableOfflineCache: false
});

export async function checkLicense(
  key: string, productId: string, deviceId: string
) {
  const result = await client.activate(
    key, deviceId, 'Desktop', app.getVersion(), productId
  );
  return {
    allowed: result.isValid,
    trial: result.isTrial,
    daysRemaining: result.trialDaysRemaining,
    message: result.message
  };
}

Recheck before sensitive or long-running paid operations, with sensible request pacing. Do not let a renderer-provided boolean become the authority that enables export. Continue with the Electron licensing tutorial for the application and Store setup.

Unity: evaluate the same key without a separate trial build

A Unity asset trial license can protect an independently distributed evaluation of an editor tool or plugin. Decide how that evaluation is distributed separately from any marketplace listing; this is not a substitute for a marketplace's own purchase or entitlement rules. Install the official Unity SDK, then call this helper from your licensing UI on Unity's main thread.

using System.Threading.Tasks;
using PermitCore;
using UnityEngine;

public static class TrialLicenseCheck
{
    public static async Task<LicenseResult> CheckAsync(
        string key, string productId, string deviceId)
    {
        var client = new PermitCoreClient(
            "https://api.permitcore.dev",
            enableOfflineCache: false,
            cacheDirectory: Application.persistentDataPath);

        return await client.ActivateAsync(
            key, deviceId: deviceId,
            deviceName: "Unity tool",
            version: Application.version,
            expectedProductId: productId);
    }
}

The helper accepts a device ID instead of pretending one platform-specific identifier solves every distribution target. Test the identifier's behavior in your packaged build and consult Unity's device identifier reference. Catch connection or SDK exceptions at the UI boundary and present a retry option. See the Unity SDK integration guide and Unity sales tutorial.

What about offline users and clock rollback?

Requiring a network connection on every launch is the simplest policy to explain, not always the best experience. A signed offline grace token lets a previously verified customer continue during a short outage. Its signature prevents editing the entitlement payload; it does not make the computer's clock trustworthy or stop someone patching your executable.

For an offline-capable trial, explicitly cap your application's accepted offline window at the earlier of the trial expiry and the permitted grace deadline. Reconnect periodically, reject invalid signatures, and do not interpret missing trial metadata in a cached result as a paid entitlement. Offline grace and post-expiry grace are different settings; test both instead of assuming the word “grace” preserves a strict fourteen-day cutoff.

Restoring an old signed cache and rolling back the local clock remain a hostile-client limitation. A fully offline first launch needs a separate signed offline activation workflow, not a call to the online trial endpoint. Read offline grace mode and offline licensing documentation before choosing that tradeoff. The three examples above deliberately avoid this ambiguity by requiring an online result.

Convert trial to paid license without reinstalling

The application should be the same binary before and after purchase. Keep projects, preferences, and customer data separate from license state. Add an Enter license key action that activates a candidate paid key against the expected product and device, then saves it only after verification succeeds. Unlock features from the returned entitlements, not from a local “purchased” flag.

With PermitCore Store, a successful Stripe checkout issues a paid license through webhook fulfillment. The customer retrieves that key from their account and enters it in the installed application. A Store purchase does not automatically mutate the earlier trial key into the paid key. Show a fulfillment-pending state while waiting, and keep payment confirmation server-side. Our Stripe-to-license walkthrough covers that flow.

If you want to keep the same key, build an explicit server-side upgrade workflow and test how it changes expiry, trial status, and features. Merely extending a date is not the same as changing every entitlement. Do not revoke the old trial before you know the paid license can be used. Any later revocation and account linking should be deliberate, auditable steps.

Measure whether the trial helps people buy

Start with a small funnel: trial issued → first successful activation → meaningful product action → checkout started → paid license activated. Track expiry and blocked repeat claims separately from connectivity failures. A user who never got the trial working is not the same as one who evaluated the product and declined to buy.

  • Activation rate: trials with a first successful activation divided by trials issued.
  • Time to value: time from first activation to a meaningful action, such as a completed export.
  • Trial-to-paid conversion: paid customers attributed to an issuance cohort, measured after that cohort has had enough time to finish the trial.
  • Recovery friction: failed device checks, reconnect prompts, and support-assisted recoveries.

These are measurement definitions, not a claim that PermitCore supplies a ready-made trial funnel dashboard. Use activation history and your own application/payment events, join them with internal IDs, and deduplicate retries. Never send license keys, raw hardware IDs, or credentials to analytics.

Test the reset paths before launch

  1. Reinstall and clear application data: recovering the original trial must not extend its server expiry.
  2. Request another trial for the same product/device: your custom issuance ledger must reuse or reject it, including concurrent requests.
  3. Use the key on a second device: a one-seat node-locked trial must not gain a second seat.
  4. Check after server-side expiry: online activation must fail, even if the local clock was moved backward.
  5. Disconnect before the first activation, during the trial, and after expiry: each case must follow the offline policy you actually chose.
  6. Buy in Stripe test mode, wait for fulfillment, and enter the paid key: existing projects must open without reinstalling.

The useful promise is not “nobody can ever bypass this.” It is that reinstalling the app no longer resets the server's record, issuing another trial is a deliberate policy decision, and paying customers can keep working in the installation they already have.

Try PermitCore — 3 months free

Put this workflow into your own app. New customers get three months free on eligible PermitCore paid plans.

Choose a plan to review the offer and billing details. This is the offer for your PermitCore account, separate from the trial you give your own customers.