Electron Licensing
Add license activation, validation, and revocation to your Electron app in 15 minutes. Supports node-locked, floating, and offline licensing.
Problem
You built an Electron app and need to prevent unauthorized copying. You need license keys that bind to a specific machine, a user-friendly activation flow, and optional offline support for air-gapped deployments. Building this from scratch means dealing with hardware fingerprinting, secure local storage, activation counting, expiry checks, and a management dashboard.
Keymint provides the licensing backend, SDK, and dashboard. You add one dependency and a few lines of code.
Architecture Diagram
Create Product
Create a product from the Keymint Dashboard. This groups your Electron app's licenses together.
- Sign in to app.keymint.dev
- Navigate to Products
- Name it after your Electron app (e.g. "MyElectronApp")
- Copy the generated product ID (starts with
prod_)
Create License Policy
When generating a license key from the dashboard, save your settings as a Template (click "Save current configuration as a template" in the Advanced tab). This lets you re-use the same configuration for future keys.
| Setting | Recommended Value | Purpose |
|---|---|---|
| licenseType | node-locked | Binds license to one machine |
| maxActivations | 1 | Single device per license key |
| expiry | None (perpetual) or 365 days | Annual subscription model |
| format | A8E2K-9F1BC-3D4GH-7J2KM | User-friendly segmented key format |
Saved Templates store reusable defaults. When you generate keys, they can prefill the template's settings — no need to re-enter limits or format every time.
Issue License
Generate license keys from the Dashboard or programmatically:
- Navigate to Licenses
- Click Generate Key
- Select your product and profile
- Set quantity, optional customer assignment
- Download or copy the generated keys
The raw license key is returned at creation. It is encrypted at rest. You can retrieve it later via GET /key with an admin API key.
Validate License
Install the Keymint SDK and node-machine-id for hardware fingerprinting:
npm install keymint node-machine-idCreate a license module in your Electron app:
import { app } from 'electron';
import { KeyMint } from 'keymint';
import { machineIdSync } from 'node-machine-id';
import Store from 'electron-store';
const client = new KeyMint(process.env.KEYMINT_CLIENT_API_KEY!);
const PRODUCT_ID = process.env.KEYMINT_PRODUCT_ID!;
const store = new Store<{ licenseKey?: string; activated?: boolean }>();
export function getStoredLicenseKey(): string | undefined {
return store.get('licenseKey');
}
export function isActivated(): boolean {
return store.get('activated') === true;
}
export async function activateLicense(licenseKey: string): Promise<{
success: boolean;
message: string;
}> {
const hostId = machineIdSync();
try {
const result = await client.activateKey({
productId: PRODUCT_ID,
licenseKey,
hostId,
});
if (result.code === 0) {
store.set('licenseKey', licenseKey);
store.set('activated', true);
return { success: true, message: result.message };
}
return { success: false, message: result.message };
} catch (error: any) {
return { success: false, message: error.message || 'Activation failed' };
}
}
export async function deactivateLicense(): Promise<boolean> {
const licenseKey = getStoredLicenseKey();
if (!licenseKey) return false;
const hostId = machineIdSync();
try {
const result = await client.deactivateKey({
productId: PRODUCT_ID,
licenseKey,
hostId,
});
if (result.code === 0) {
store.delete('licenseKey');
store.delete('activated');
return true;
}
return false;
} catch {
return false;
}
}In your Electron main process, gate app launch behind license validation:
import { app, BrowserWindow } from 'electron';
import { activateLicense, getStoredLicenseKey, isActivated } from './license';
app.whenReady().then(async () => {
if (isActivated()) {
createWindow();
return;
}
const storedKey = getStoredLicenseKey();
if (storedKey) {
const result = await activateLicense(storedKey);
if (result.success) {
createWindow();
return;
}
}
// Show activation window — user enters key, calls activateLicense()
createActivationWindow();
});
function createWindow() {
const win = new BrowserWindow({ width: 1200, height: 800 });
win.loadFile('index.html');
}
function createActivationWindow() {
const win = new BrowserWindow({ width: 480, height: 320 });
win.loadFile('activation.html');
}
// Deactivate on uninstall (optional)
app.on('before-quit', async () => {
const { deactivateLicense } = await import('./license');
await deactivateLicense();
});Handle Activations
Build a simple activation UI using Electron's IPC:
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('licenseAPI', {
activate: (key: string) => ipcRenderer.invoke('license:activate', key),
getStatus: () => ipcRenderer.invoke('license:status'),
});import { ipcMain } from 'electron';
import { activateLicense, isActivated, getStoredLicenseKey } from './license';
ipcMain.handle('license:activate', async (_event, licenseKey: string) => {
return activateLicense(licenseKey);
});
ipcMain.handle('license:status', () => {
return {
activated: isActivated(),
licenseKey: getStoredLicenseKey() ? '••••••••' : null,
};
});<!DOCTYPE html>
<html>
<body>
<h2>Activate License</h2>
<input id="key" type="text" placeholder="A8E2K-9F1BC-3D4GH-7J2KM" />
<button id="activate">Activate</button>
<p id="status"></p>
<script>
const status = document.getElementById('status');
document.getElementById('activate').addEventListener('click', async () => {
status.textContent = 'Activating...';
const key = document.getElementById('key').value.trim();
const result = await window.licenseAPI.activate(key);
status.textContent = result.success
? 'License valid!'
: `Failed: ${result.message}`;
});
</script>
</body>
</html>Handle Revocations
You can block a license from the Dashboard or programmatically:
// From your backend (admin API key required)
await admin.blockKey({
productId: "prod_Nx8K2mLpQ4rVtW9sBc",
licenseKey: "A8E2K-9F1BC-3D4GH-7J2KM",
});When a license is blocked, your Electron app will detect it on the next activation attempt. The activateKey call returns code !== 0 with a message like "Invalid license key or insufficient permissions".
To handle this in your app:
export async function checkLicenseHealth(): Promise<boolean> {
const licenseKey = getStoredLicenseKey();
if (!licenseKey) return false;
const hostId = machineIdSync();
try {
const result = await client.activateKey({
productId: PRODUCT_ID,
licenseKey,
hostId,
});
return result.code === 0;
} catch {
return false;
}
}
// Run periodically (e.g. once per day)
setInterval(async () => {
const healthy = await checkLicenseHealth();
if (!healthy) {
store.delete('activated');
// Notify renderer to show re-activation dialog
}
}, 24 * 60 * 60 * 1000);Production Considerations
API Key Security
- Bundle the
client-scoped API key in your Electron app. This is the only scope safe for distribution — it can only activate/deactivate/checkout. - Never bundle
adminorread-onlyAPI keys. These can read all customer data and manage all licenses. - Rotate API keys from the Dashboard every 90 days.
Host ID Stability
Choose a host ID strategy that survives OS reinstalls and hardware changes:
| Method | Stability | Example |
|---|---|---|
node-machine-id (default) | Survives OS reinstall | machineIdSync() |
| MAC address | Changes with network adapters | Not recommended |
| Custom mix (CPU + motherboard) | Survives most changes | Combine multiple identifiers |
If
hostIdchanges unexpectedly, the activation counts as a new device and consumes another seat. Use a stable, predictable host ID.
Offline Support
If your Electron app needs to work offline, use Keymint's offline licensing:
// From your backend
const offlineLicense = await admin.signOfflineKey({
productId: "prod_Nx8K2mLpQ4rVtW9sBc",
licenseKey: "A8E2K-9F1BC-3D4GH-7J2KM",
hostId: "machine-fingerprint",
ttl: 60 * 60 * 24 * 30, // 30 days
});
// offlineLicense.file is a JSON string containing the signed JWT
// Save this file and distribute it to the air-gapped machineThen verify offline in Electron using jose:
import { jwtVerify, decodeProtectedHeader, importSPKI } from 'jose';
import * as fs from 'fs';
const TRUSTED_KEYS = new Map([
['AbC123XyZ', '-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAr7Z8s2Z...\n-----END PUBLIC KEY-----']
]);
export async function verifyOfflineLicense(filePath: string, machineCode: string) {
const fileContent = JSON.parse(fs.readFileSync(filePath, 'utf8'));
const { signedKey } = fileContent;
const { kid } = decodeProtectedHeader(signedKey);
if (!kid) throw new Error('Invalid token: missing kid');
const pem = TRUSTED_KEYS.get(kid);
if (!pem) throw new Error('Untrusted key ID');
const publicKey = await importSPKI(pem, 'EdDSA');
const { payload } = await jwtVerify(signedKey, publicKey, {
algorithms: ['EdDSA'],
});
if (payload.machineCode !== machineCode) throw new Error('Hardware mismatch');
return payload;
}Offline licensing is available on all plans. See Offline Verification for the full workflow.
Code Signing
Always code-sign your Electron app. Unsigned apps on macOS trigger Gatekeeper warnings that discourage users from entering license keys.
Auto-Updates
If you use electron-updater, store the license key and activation state outside the app bundle (e.g. in app.getPath('userData')) so updates don't wipe the license.
Complete Source Code
Full Electron license module
// src/main/license.ts
import { app } from 'electron';
import { KeyMint } from 'keymint';
import { machineIdSync } from 'node-machine-id';
import Store from 'electron-store';
interface LicenseStore {
licenseKey?: string;
activated?: boolean;
lastCheck?: number;
}
const client = new KeyMint(process.env.KEYMINT_CLIENT_API_KEY!);
const PRODUCT_ID = process.env.KEYMINT_PRODUCT_ID!;
const store = new Store<LicenseStore>();
const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
export function getStoredLicenseKey(): string | undefined {
return store.get('licenseKey');
}
export function isActivated(): boolean {
return store.get('activated') === true;
}
export function getHostId(): string {
return machineIdSync();
}
export async function activateLicense(licenseKey: string): Promise<{
success: boolean;
message: string;
}> {
const hostId = getHostId();
try {
const result = await client.activateKey({
productId: PRODUCT_ID,
licenseKey,
hostId,
});
if (result.code === 0) {
store.set('licenseKey', licenseKey);
store.set('activated', true);
store.set('lastCheck', Date.now());
return { success: true, message: result.message };
}
return { success: false, message: result.message };
} catch (error: any) {
return {
success: false,
message: error.message || 'Network error during activation',
};
}
}
export async function deactivateLicense(): Promise<boolean> {
const licenseKey = getStoredLicenseKey();
if (!licenseKey) return false;
const hostId = getHostId();
try {
await client.deactivateKey({
productId: PRODUCT_ID,
licenseKey,
hostId,
});
store.delete('licenseKey');
store.delete('activated');
store.delete('lastCheck');
return true;
} catch {
return false;
}
}
export async function checkLicenseHealth(): Promise<boolean> {
const licenseKey = getStoredLicenseKey();
if (!licenseKey) return false;
// Skip if checked recently
const lastCheck = store.get('lastCheck') ?? 0;
if (Date.now() - lastCheck < CHECK_INTERVAL) return isActivated();
const hostId = getHostId();
try {
const result = await client.activateKey({
productId: PRODUCT_ID,
licenseKey,
hostId,
});
const valid = result.code === 0;
store.set('activated', valid);
store.set('lastCheck', Date.now());
return valid;
} catch {
// Network error — don't invalidate, but flag for next check
return isActivated();
}
}
export function startHealthCheck(interval = CHECK_INTERVAL) {
setInterval(async () => {
const healthy = await checkLicenseHealth();
if (!healthy) {
store.set('activated', false);
}
}, interval);
}