Skip to content
Skip to content

Licensing a JavaScript desktop app

This guide adds licensing to a desktop application written in JavaScript — Electron, Tauri, or anything else that ships a JS runtime to a customer's machine — using @casazium/license-sdk.

Everything here is also doable with plain HTTP calls; the SDK is a convenience layer over the API, not a separate system.

What you are building

Four moments matter, and most licensing bugs come from confusing them:

  1. Activation — once, when a customer first enters their key on this machine. It consumes a seat and returns a token you must keep.
  2. The launch check — every time the app starts, to confirm the key is still valid.
  3. Being offline — the app must keep working when the network does not. This is the part people get wrong.
  4. Moving machines — freeing the seat so the same key works on a new laptop.

Install

bash
npm install @casazium/license-sdk
js
import { CasaziumLicenseClient } from '@casazium/license-sdk';

const client = new CasaziumLicenseClient({
  baseUrl: process.env.LICENSE_SERVER_URL ?? 'https://license.example.com/v1',
});

Read the base URL from configuration, not a hardcoded string. If you ever move between the hosted tier and your own server — in either direction — a hardcoded URL means shipping an update to every customer.

1. Activate on first run

Activation binds the key to one machine. instance_id is the label you choose for that machine, and the SDK ships a helper that derives a stable one:

js
import { getFingerprint } from '@casazium/license-sdk/fingerprint';

const key = await promptCustomerForKey();
const instanceId = getFingerprint();

const { activated, token } = await client.activate(key, instanceId);

if (activated) {
  await saveLicense({ key, token, instanceId });
}

Keep the token. It is returned only once, by the call that creates the activation, and it is the credential required to free that seat later. Lose it and your customer cannot move machines without asking you to intervene.

getFingerprint() is Node-only

It uses node-machine-id, which needs a Node runtime. In Electron that means the main process. In Tauri, whose front-end is a browser context, generate a UUID once, persist it, and pass that as instance_id — it is a caller-supplied label, and the server does not require it to be a real machine identifier.

2. Check on every launch

js
const { key } = await loadLicense();
const result = await client.verifyKey(key);

if (result.valid) {
  await recordSuccessfulCheck();   // see step 3
  startApp();
} else {
  showLicenseScreen(result);
}

verifyKey is a public endpoint — no admin credential — and sits in the verification rate-limit tier. Once per launch is well within it. Do not call it in a loop, on a timer, or on every window focus.

3. Handle being offline — properly

A launch check that fails closed will lock a paying customer out of your application because their train went into a tunnel. A network error is not an invalid license. Distinguish the two:

js
try {
  const result = await client.verifyKey(key);
  if (!result.valid) return showLicenseScreen(result);   // a real answer: revoked, expired
  await recordSuccessfulCheck();
} catch {
  // No answer at all. Fall back to the last one we got.
  if (await withinGracePeriod({ days: 14 })) {
    startApp();
  } else {
    showOfflineTooLongScreen();
  }
}

Pick a grace period long enough to cover a normal holiday. Fourteen days is a reasonable default; it is your decision, not the server's.

For a stronger offline story, issue a signed license file and verify it locally with no network at all:

js
const offlineClient = new CasaziumLicenseClient({ publicKey });
const ok = await offlineClient.verifySignedFile({ license, signature });

The signature is checked against your server's public key, so a file cannot be forged or edited — see License Verification for the issuing side.

4. Let customers move machines

js
const { key, token } = await loadLicense();
await client.deactivate(key, token);
await clearLocalLicense();

Put this behind a visible "deactivate this computer" control. The alternative is an email to you every time somebody buys a laptop.

Since v1.2.0 there is also an end-user portal your customers can use themselves, with no account and no involvement from you — worth enabling if you would rather not build this screen.

5. Optional: check for updates

If you distribute builds through License Server, the same key can gate update checks:

js
const { update_available, release } = await client.checkUpdate({
  key,
  productId: 'your-product-id',
  platform: 'darwin-arm64',
  currentVersion: app.getVersion(),
});

if (update_available) offerUpdate(release);

It resolves to { update_available: false, release: null } when there is nothing newer rather than throwing, so it is safe on every launch. See Software Distribution.

Where the code goes

Electron. Put all of it in the main process and expose only narrow IPC handlers to the renderer — activate(key), getStatus(), deactivate(). The renderer is a browser window; treat it as untrusted. Store the key and token with safeStorage, which uses the OS keychain, not in localStorage and not in plain JSON beside your app.

Tauri. Either call the SDK from the front-end — it ships a browser-safe build that is selected automatically by Vite and other bundlers — or do the licensing in Rust and expose commands to the front-end. Prefer Rust if you want the key out of the web context. Use the OS keychain rather than localStorage.

What not to do

  • Never ship an admin API key. Anything that issues, lists or revokes licenses belongs on a server you control. The SDK's browser build omits listLicenses() and revoke() for exactly this reason.
  • Do not treat a network failure as an invalid license (step 3).
  • Do not verify only in the renderer, where a user can open dev tools and change the answer. Enforce in the main process or in Rust.
  • Do not hardcode the base URL — configuration, so moving hosts is not a release.
  • Do not check on a timer. Once per launch, plus after the customer enters a key.

A note on what licensing can and cannot do

A determined person with a debugger can patch out any client-side check, in any language. Licensing is not copy protection — it exists so that honest customers can pay you, manage their own seats, and be told when something expires. Build it to be unobtrusive when it works and forgiving when the network fails, and it will cost you far less support than a system that treats every customer as a suspect.

Next steps