GuidesUpdated 2026-11-05View as MarkdownEdit on GitHub

Electron & desktop apps

Sell a desktop app with license keys: activate the key on first launch, limit how many devices can use it, check it on startup, and keep working offline for a few days.

You need: Electron 28+, a Keyvane project with a product, and the project's publishable key (kv_pk_…). Never ship a secret key in an app.

Where the code runs

Call Keyvane from the main process only and expose a narrow API to the renderer over IPC. Store the key encrypted with Electron's safeStorage.

npm install @keyvane/sdk
// main/license.ts
import { app, ipcMain, safeStorage } from 'electron';
import { createHash } from 'node:crypto';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { arch, cpus, hostname, platform } from 'node:os';
import { join } from 'node:path';
import { Keyvane, KeyvaneConnectionError } from '@keyvane/sdk';

const keyvane = new Keyvane({ apiKey: 'kv_pk_...', baseUrl: 'https://keyvane.dev', timeoutMs: 5_000 });
const file = join(app.getPath('userData'), 'license.bin');
const OFFLINE_DAYS = 7;

const fingerprint = () => createHash('sha256').update([hostname(), platform(), arch(), cpus()[0]?.model].join('|')).digest('hex').slice(0, 32);

type Saved = { key: string; validatedAt?: number; features?: Record<string, boolean> };
const load = (): Saved | null => (existsSync(file) ? JSON.parse(safeStorage.decryptString(readFileSync(file))) : null);
const save = (s: Saved) => writeFileSync(file, safeStorage.encryptString(JSON.stringify(s)));

ipcMain.handle('license:activate', async (_e, key: string) => {
  const result = await keyvane.licenses.activate({ key, fingerprint: fingerprint(), name: `${hostname()} (${platform()})` });
  if (!result.valid) return { ok: false, code: result.code, message: result.message };
  save({ key: result.license!.key, validatedAt: Date.now(), features: { export: result.entitlements?.can('export') ?? false } });
  return { ok: true, plan: result.license?.plan?.name };
});

ipcMain.handle('license:status', async () => {
  const saved = load();
  if (!saved) return { ok: false, code: 'no_license' };
  try {
    const result = await keyvane.licenses.validate({ key: saved.key, fingerprint: fingerprint() });
    if (result.valid) save({ ...saved, validatedAt: Date.now(), features: { export: result.entitlements?.can('export') ?? false } });
    return { ok: result.valid, code: result.code, message: result.message, features: result.valid ? load()?.features : {} };
  } catch (err) {
    const fresh = saved.validatedAt && Date.now() - saved.validatedAt < OFFLINE_DAYS * 86_400_000;
    if (err instanceof KeyvaneConnectionError && fresh) return { ok: true, offline: true, features: saved.features };
    throw err;
  }
});

ipcMain.handle('license:deactivate', async () => {
  const saved = load();
  if (saved) await keyvane.licenses.deactivate({ key: saved.key, fingerprint: fingerprint() });
});

What your customers experience

Situation Result
First launch Enter the key once; the device is activated
Key used on too many devices activation_limit_reached: offer to deactivate another device
Subscription canceled or key revoked valid: false on the next check, with a reason you can show
Offline Works for up to 7 days after the last successful check

You can see and remove each device on the license page of the Keyvane dashboard. The license CLI example implements the same flow without Electron.

Next: License keys & devices