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

mermaid
Rendering diagram...

Create Product

Create a product from the Keymint Dashboard. This groups your Electron app's licenses together.

  1. Sign in to app.keymint.dev
  2. Navigate to Products
  3. Name it after your Electron app (e.g. "MyElectronApp")
  4. 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.

SettingRecommended ValuePurpose
licenseTypenode-lockedBinds license to one machine
maxActivations1Single device per license key
expiryNone (perpetual) or 365 daysAnnual subscription model
formatA8E2K-9F1BC-3D4GH-7J2KMUser-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:

  1. Navigate to Licenses
  2. Click Generate Key
  3. Select your product and profile
  4. Set quantity, optional customer assignment
  5. 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:

bash
npm install keymint node-machine-id

Create a license module in your Electron app:

typescript
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:

typescript
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:

typescript
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('licenseAPI', {
  activate: (key: string) => ipcRenderer.invoke('license:activate', key),
  getStatus: () => ipcRenderer.invoke('license:status'),
});
typescript
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,
  };
});
html
<!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:

typescript
// 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:

typescript
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 admin or read-only API 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:

MethodStabilityExample
node-machine-id (default)Survives OS reinstallmachineIdSync()
MAC addressChanges with network adaptersNot recommended
Custom mix (CPU + motherboard)Survives most changesCombine multiple identifiers

If hostId changes 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:

typescript
// 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 machine

Then verify offline in Electron using jose:

typescript
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
typescript
// 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);
}