Node.js

Node.js license key validation for Electron and CLI apps

To validate a license key in a Node.js app, call your license server when the app starts, verify the Ed25519 signature on the answer with a public key built into your code, and only then use the license data. In Electron, run that check in the main process and give the renderer only the verdict. This guide shows how with velsigil-client, the dependency-free Node.js SDK included with Velsigil.

Last updated

Requirements and installation

The SDK needs Node.js 18 or later: it uses the built-in fetch, AbortController and Ed25519 from node:crypto, and has no runtime dependencies. It is written in TypeScript and builds to both ES modules and CommonJS, with type definitions.

It is included with Velsigil as source code and is not on npm. Build it in the Velsigil source tree, then add it to your app as a local package:

Shell
# In the Velsigil source tree (after npm install at its root)
npm run build -w sdks/node        # builds dist/esm and dist/cjs

# Or pack it as a tarball (prepack runs the same build)
cd sdks/node
npm pack                          # writes velsigil-client-1.0.0.tgz

# In your application
npm install ../path/to/velsigil-client-1.0.0.tgz

Import it with import { VelsigilClient } from 'velsigil-client' in ES modules, or require('velsigil-client') in CommonJS.

Quick start

Copy the API URL, product ID and public key from the product’s Integration tab and keep them in code. Never load the public key from a file or setting the user can change: with their own key, they could run a fake server.

license.ts
import { VelsigilClient, VelsigilError, FileStore, defaultStoreDirectory } from 'velsigil-client';

const API_URL = 'https://licenses.example.com';           // from the product's Integration tab
const PRODUCT_ID = '<product id>';
const PUBLIC_KEY = '<Ed25519 public key, base64>';         // compiled in, never user-editable

let client: VelsigilClient;
try {
  client = new VelsigilClient(API_URL, PRODUCT_ID, PUBLIC_KEY, {
    store: new FileStore(defaultStoreDirectory('MyApp')),  // device secret + offline lease
  });
} catch (e) {
  if (e instanceof VelsigilError) { console.error(`config: ${e.code}`); process.exit(2); }
  throw e;
}

const result = await client.validateWithOfflineFallback(licenseKey, { version: '1.2.0' });
if (!result.ok) {                                          // business failures never throw
  console.error(result.code, result.message, result.requestId);
  process.exit(1);
}
console.log(result.license?.plan, result.offline, result.expiresAt, result.daysRemaining());
if (result.hasFeature('pro')) enablePro();

Only setup code throws a VelsigilError: the client constructor (invalid_configuration, invalid_public_key or hwid_unavailable) and FileStore or defaultStoreDirectory (invalid_argument), which is why the quick start creates both inside one try block. License calls never throw: validate and the other license calls resolve to a frozen VelsigilResult, and downloadRelease resolves to a DownloadFileResult, so there is no try/catch around validate. result.expiresAt is a Date, daysRemaining() rounds down and returns null for lifetime licenses, and hasFeature() is always false for failures.

Electron: keep the check in the main process

A renderer is a web page: its code and state are easy to inspect and change from the developer tools. Run the SDK in the main process, keep the client and the license key there, and expose only the verdict over IPC:

main.js
// main.js (main process)
const { app, ipcMain } = require('electron');
const { VelsigilClient, FileStore, defaultStoreDirectory } = require('velsigil-client');

const client = new VelsigilClient(API_URL, PRODUCT_ID, PUBLIC_KEY, {
  store: new FileStore(defaultStoreDirectory('MyApp')),
});
let license = null;

app.whenReady().then(async () => {
  license = await client.validateWithOfflineFallback(loadLicenseKey(), { version: app.getVersion() });
  createWindow();
});

// The renderer gets the verdict: never the key, the device secret or the client.
ipcMain.handle('license:status', () => ({
  licensed: license?.ok === true,
  pro: license?.hasFeature('pro') === true,
  offline: license?.offline === true,
}));
preload.js
// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('license', {
  status: () => ipcRenderer.invoke('license:status'),
});

Then make the main process enforce it too: a renderer that is patched to report pro: true should still not receive pro data or actions from the main process.

Configuration

OptionDefaultUse
timeout15000 msConnect plus full response, 1 to 600,000 ms.
storeMemoryStoreUse FileStore in real apps; memory is lost on exit.
hwidmachine ID hashOverride with a stable value of 8 to 256 printable characters.
fetchglobal fetchA custom fetch, for example one with a proxy dispatcher.
onStoreErrornoneCalled when the store fails; the SDK keeps working from memory.
allowInsecureHttpfalsePlain HTTP to other hosts than localhost. Test setups only.

The API URL must use HTTPS (plain HTTP only for localhost, 127.0.0.1 and [::1]) and must not contain credentials, a query or a fragment. The SDK appends /api/client/v1 itself.

Offline leases, device secret and clock skew

After a successful validation the SDK stores the signed lease automatically; how many hours it lasts is set by the seller per product, not by a client option. validateWithOfflineFallback uses it only on network_error. In the Node.js SDK, if a lease is stored but expired or invalid, the fallback returns lease_expired or lease_invalid; only without any stored lease do you get the original network_error.

FileStore writes one velsigil-<productId>.json per product atomically (mode 0600, directory 0700). defaultStoreDirectory('MyApp') is %LOCALAPPDATA%\MyApp\velsigil on Windows, ~/Library/Application Support/MyApp/velsigil on macOS and $XDG_DATA_HOME/MyApp/velsigil on Linux. Stores never receive the license key. Call clearStoredState() when the user switches to a different key, or after a device reset in the portal.

If the local clock is too far off, the server answers with a signed clock_skew that includes its time. The SDK learns the offset from that verified answer, retries once with a new nonce, and keeps the offset in client.clockOffset.

Updates and downloads

update.mjs
import { join } from 'node:path';

const update = await client.checkUpdate(APP_VERSION);
if (update.ok && update.update?.updateAvailable) {
  const link = await client.getDownload(licenseKey, update.update.latestVersion);
  if (link.ok && link.download) {
    // Use a safe local name, never the server's file name as a path.
    const file = await client.downloadRelease(link.download, join(downloadsDir, 'update.zip'), {
      onProgress: (received, total) => render(received / total),
    });
    if (file.ok) installUpdate(file.path); // size and SHA-256 verified against the signed answer
  }
}

update.mandatory, or outdated_version from a validation, means the user must update before continuing. Download links are short-lived; request a new one if it expires. Non-HTTPS links are refused, and the file only appears at the destination after its size and SHA-256 match the signed values.

Concurrency

A client is safe to use from concurrent async code. Device-bound calls (validate, deactivate, getDownload, validateOffline, clearStoredState) are serialized per client, so a freshly issued device secret is saved before the next request goes out; checkUpdate runs unserialized. Use one client per product per process. worker_threads do not share memory, so give each thread its own client, or better, let one thread own validation. Processes sharing a store directory never see torn files, but the last writer wins: avoid validating the same license from several processes at the moment a device first activates.

Hardening JavaScript apps

JavaScript ships as readable source. Bundling, minification, obfuscators and single-executable packaging raise the effort needed to patch a check, but they cannot prevent it.

  • Keep the public key, product ID and API URL in code, not in a config file, environment variable or registry key the user controls.
  • Check results in several places, re-validate periodically and treat every non-ok result as unlicensed.
  • Let your server enforce what is valuable: downloads, cloud features and content. The SDK only reports what the server decided.
  • Protect a remembered license key like a password, in the OS keychain or a per-user file, and never log keys or device secrets.
  • Keep offline leases short and never enable allowInsecureHttp in production.

CLI tools and Node.js services

In a command-line tool, validate at the start of each command that needs a license and exit with distinct codes, as the quick start does (2 for a configuration problem, 1 for a license failure), so scripts can tell them apart. Honor result.retryAfter on rate_limited instead of retrying in a loop: the server allows 60 requests a minute per IP and 30 per license key. For a long-running Node.js service you ship to customers, validate at startup and then every few hours, and keep serving from the offline lease during short outages.

Key points

  • Build the SDK from the Velsigil source tree and add it as a local package; it is not on npm.
  • Only setup code (the client and its store) throws; license calls resolve to a result, and only a verified answer gives ok === true.
  • In Electron, validate in the main process and send the renderer only the verdict.
  • Use FileStore(defaultStoreDirectory(...)) so the device secret and offline lease persist.
  • Minification and packaging slow down patching but cannot stop it; let your server enforce what is valuable.

Keep reading

Offline license activation

How software checks a license without internet: signed offline leases, signed license files, grace periods, clock tampering and what each one trusts.

License key security

How license keys get shared, forged or replayed, and the defenses that work: signed answers, replay protection, hashed keys, device secrets, rate limits.

Hardware-locked licensing

How hardware-locked licenses bind a key to a device: machine IDs per OS, why hardware IDs can be spoofed, device secrets, limits and customer resets.

Python license key validation

Add license key validation to a Python app: verify signed server answers, bind devices, cache offline leases and know the limits of bytecode obfuscation.

All guides

Early access

Request early access

Early access is free. In exchange, we ask for your feedback while we prepare the paid launch. Spots are limited, and we reply to every request by email.

  • Free until paid subscriptions launch
  • 30% off your first year on either plan when they do
  • For businesses established in the United States

Early access ends when paid subscriptions launch; we will tell members by email before then. To keep using Velsigil after that, you need a subscription. The 30% discount applies to the first 12 months of either plan, paid monthly or yearly.

Request early access

Opens your email app with a short template: company, what you sell, your platforms and languages, expected license volume and how you plan to deploy. You can also write to hello@velsigil.com.