Skip to Content
ElectronInstallation and setup

Installation and setup

Install

npm install @licenseman/electron

electron 28 or later is a peer dependency. The package also installs @napi-rs/keyring as an optional dependency, a native module the SDK uses to reach the system keychain: see packaging.

Create the LicenseManager

Copy your product’s id and public key

Open Security in the sidebar: copy the public key, and the id of the product, like x7Kp2QaZ.

Create one instance, in the main process

Create it once the app is ready, and keep it for the lifetime of the app:

main.ts
import { app } from "electron"; import { LicenseManager } from "@licenseman/electron"; const PRODUCT_ID = "x7Kp2QaZ"; const PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEA… -----END PUBLIC KEY-----`; let license: LicenseManager; app.whenReady().then(async () => { license = new LicenseManager({ productId: PRODUCT_ID, publicKey: PUBLIC_KEY }); license.onStateChange((state) => sendStateToWindows(state)); const state = await license.ready(); if (state === "ACTIVE") openMainWindow(); else openActivationWindow(); });

Dispose it when the app quits

main.ts
app.on("will-quit", () => license?.dispose());

dispose() stops the background re-validation and the hourly clock check. Their timers don’t keep the process alive either way, so forgetting it doesn’t prevent the app from quitting.

Hard-code the public key and the product id in your source code. Never load them from a file next to your app, or fetch them at runtime: whoever replaces the public key can sign their own tokens, and your app would trust them. See the public key.

What happens on construction

The constructor returns right away, and starts in the background:

  1. It reads the stored key. No key: the state becomes MISSING_KEY before the constructor returns, and ready() resolves with it.
  2. With a key and a valid stored token, the state becomes ACTIVE, and ready() resolves with it right away. The online validation runs in the background: when it refuses the license, the state changes, and your listener is called.
  3. With a key but no valid stored token, it validates the key online, retrying for up to 30 seconds, and ready() resolves once it’s done, with ACTIVE or the reason the app is locked.
  4. It starts the clock check, now and every hour.

The constructor throws only when productId or publicKey is empty. Everything else, a missing keychain, no network, a broken fingerprint command, is handled without throwing.

Because the MISSING_KEY state is set during the constructor, a listener added afterwards never sees that change. Always use the value ready() resolves with, or getState(), for the first screen, and the listener for the changes after it.

One instance per product

Treat the LicenseManager as a singleton. Two instances with the same productId share the same stored data, but not their state or timers: they’d both validate in the background, and could disagree after one of them activates a new key.

An app selling several products, like an app and a paid plugin with their own licenses, creates one instance per product, each with its own productId. Their keys, tokens and fingerprints are stored separately and never overlap.

Options

Most of the time, you only need productId and publicKey. The other options have defaults that fit most apps.

new LicenseManager({ productId: "x7Kp2QaZ", // required publicKey: PUBLIC_KEY, // required version: "2.3.1", // default: app.getVersion() machineName: "Jane's MacBook", // default: the hostname platform: "macOS", // default: macOS, Windows or Linux fingerprint: () => readMyId(), // default: the system's machine id minOfflineDurationMs: 3 * 24 * 60 * 60 * 1000, // default: 0 });
OptionDefaultWhat it does
productIdRequiredThe id of your product. Checked in every token’s aud, and used to keep each product’s stored data apart. Shown on the Security page.
publicKeyRequiredYour organization’s Ed25519 public key, as the PEM shown on the Security page. Verifies every token.
versionapp.getVersion()The version of your app, sent with every validation and checked offline against the token’s mv.
machineNameos.hostname()The name of the machine, shown to you and in the customer portal.
platformmacOS, Windows or LinuxThe machine’s platform. On other systems, Node’s process.platform, like freebsd.
fingerprintThe system’s machine idThe machine part of the fingerprint: a string, or a function returning one, sync or async. See machine fingerprint.
minOfflineDurationMs0How long, at least, your app must keep working offline after a renewal. Makes renewals happen earlier. See renewal.
apiBaseUrlhttps://api.licensemanager.devFor tests only: another API origin. Trailing slashes are removed.
storageDirsPer systemFor tests only: the folders of the file fallback.

version

By default, it’s Electron’s app.getVersion(), the version of your app’s package.json. Two cases to know:

  • In development, when your package.json has no version, app.getVersion() returns Electron’s version, like 33.2.0, which can be refused with version_not_allowed. Set a version in package.json, or pass version.
  • Outside Electron, like in unit tests run by Node.js, there’s no app: pass version, or every validation fails. In setLicenseKey, it throws version is required when not running in Electron; in the background, it’s handled like a network error.

machineName and platform

Both are sent with every validation, and replace what LicenseManager knows about the machine. The hostname often holds the customer’s name, like Janes-MacBook-Pro.local, which is what makes machines easy to recognize in the customer portal. Pass your own value if you’d rather not send it.

Development and release builds

Point your development builds at a test product, and your release builds at the live one. Both share the same public key, which is one per organization:

main.ts
const PRODUCT_ID = app.isPackaged ? "x7Kp2QaZ" : "t3Hw9LmB";

A test key validated against the live product answers not_found, and the opposite too: the SDK shows INVALID_KEY.

Since both products share the public key, the product id is what stops a test key from unlocking your release build: the SDK checks it in every token. Make sure release builds can’t end up with the test product’s id, like from an environment variable a customer could set.

Packaging

The keychain is reached through @napi-rs/keyring, a native module. When it can’t be loaded, the SDK silently falls back to files: your app keeps working, but the key and token are less protected. Check these when you package your app:

  • Keep it in your dependencies. Don’t install with --no-optional, or --omit=optional: it’s an optional dependency.
  • Install it for each target. It ships one binary per system and architecture, like @napi-rs/keyring-darwin-arm64, and your package manager only installs the one of the computer it runs on. Build each platform on its own system, like with one CI job per platform, or install the binaries of the platforms you target. For a universal macOS app, you need both darwin-arm64 and darwin-x64.
  • Unpack it from the asar archive. Native modules can’t be loaded from inside app.asar. electron-builder unpacks them by default; with Electron Forge, add @electron-forge/plugin-auto-unpack-natives.
  • Don’t bundle it. If you bundle your main process with Vite, webpack or esbuild, mark @napi-rs/keyring as external: the SDK loads it with require at runtime, and a bundler that can’t see it leaves it out, without an error.

To check it works, activate a key in your packaged app, then look for items of the service dev.licensemanager.sdk in the Keychain Access app on macOS, or the Credential Manager on Windows.

Network access

The SDK makes two kinds of requests, from the main process, with Node’s fetch:

RequestWhenTimeout
POST https://api.licensemanager.dev/v1/products/{productId}/validate-keyEach validation10 seconds
HEAD https://google.com, to read the time from its Date headerOn launch, then every hour, for the clock check5 seconds

Requests from the main process don’t go through your windows’ Content Security Policy. They don’t go through the proxy configured in Chromium either: on a network that only allows traffic through a proxy, they fail, and the SDK handles it like being offline.