Guide · By Buğra Ömer Aydemir · Updated 15 October 2026

How to add license keys to an Electron app that keep working offline

This guide adds license keys to an Electron app with Carcha Seal: the customer types a key once, the app gets a lease signed with Ed25519, and from then on it checks the license on the computer without a network call. It takes about ten lines in the main process. You can follow it with the public demo server and a trial key, without signing up.

How it works

There are three parts: a license server that holds your products, policies and licenses; the SDK in your app's main process; and an admin console. When the customer enters a key, the SDK sends it with a SHA-256 hash of the computer's machine ID. The server takes a seat and returns a lease: a compact JSON Web Signature (RFC 7515) signed with Ed25519 (RFC 8032). Your app ships only the public key, so it can verify a lease but cannot create one.

A lease names one device, one product and a check-in deadline. On the default policy the app checks in every 72 hours; if the server cannot be reached it keeps working for 7 more days of offline grace. Both numbers are per-product policy settings.

1. Install the SDK

The SDK runs in the Electron main process or in plain Node.js 22.13 or later.

npm install @carchalabs/seal-electron

Carcha Seal was previously named LicenseKit; the SDK is now @carchalabs/seal-electron and its class Seal (LicenseKit still works as an alias). To try the flow first, get a trial key in the Try it section and run npx @carchalabs/seal-quickstart activate <key>.

2. Create the SDK in the main process

Keep licensing in the main process. The renderer asks for the state over IPC and never sees keys or leases. Pass the current and the next public key, so the server can rotate its signing key without an app update; the server publishes both at /v1/keys.

// main.js
const { app, ipcMain, safeStorage } = require('electron');
const { Seal } = require('@carchalabs/seal-electron');

let kit;

app.whenReady().then(async () => {
  kit = new Seal({
    serverUrl: 'https://licenses.example.com',
    product: 'example-studio',
    publicKey: [currentKeyPem, nextKeyPem],
    storageDir: app.getPath('userData'),
    appVersion: app.getVersion(),
    secureStore: {
      encrypt: (s) => safeStorage.encryptString(s),
      decrypt: (b) => safeStorage.decryptString(b),
    },
  });
  // … create your window, then step 4
});

secureStore encrypts the stored lease with Electron's safeStorage, which uses the operating system's keychain or credential store. On Linux without a keyring, Electron falls back to a fixed password; the SDK reports that as storageProtected: false.

3. Activate a key when the customer enters it

Activate once, when the customer types the key, not on every launch. Activating again on the same computer reuses its seat.

ipcMain.handle('license:activate', async (_event, licenseKey) => {
  try {
    return await kit.activate(licenseKey);   // returns the new state
  } catch (err) {
    // err.code: the server's error code, 'network', 'clock_suspect' or a lease failure reason
    return { status: 'invalid', reason: err.code, message: err.message };
  }
});

Keys look like LK-7Q2M-9XPA-K4TD-W8NE. Input ignores case, spaces and hyphens, and the alphabet has no I, L, O or U, so customers cannot mistype one letter for another. The server stores only a SHA-256 hash of each key.

4. Check at startup and every few hours

check() checks in with the server and stores the new lease. If the server cannot be reached, it decides from the stored lease, offline. Handle every status:

async function refreshLicense(win) {
  const state = await kit.check();
  switch (state.status) {
    case 'valid':          // licensed
    case 'grace':          // offline past the check-in deadline; works until state.revalidateBy
      break;
    case 'unlicensed':     // no key on this computer: show the key form
    case 'invalid':        // state.reason says why: expired, revoked, wrong_device, …
    case 'clock_suspect':  // the clock moved back; paid features stop until an online check
      break;
  }
  win.webContents.send('license:state', state);
}

setInterval(() => refreshLicense(win), 4 * 60 * 60 * 1000);

Show state.message to the user and log state.reason. grace and clock_suspect need their own wording.

5. Gate features

Seal.hasFeature(state, 'export-pdf') is true only for valid and grace states that include the feature. A check like this can be patched out of a JavaScript app, so for features that matter, encrypt the code or data at build time and open it with kit.decrypt or kit.requireEncrypted. Then an app with its checks removed still has nothing to run. See Encrypting data and code for a feature.

if (Seal.hasFeature(state, 'export-pdf')) {
  const premium = await kit.requireEncrypted('export-pdf', join(__dirname, 'premium', 'index.js.lke'));
  premium.exportReport(doc);
}

6. Let customers move to another computer

kit.deactivate() frees this computer's seat and deletes the stored lease, so the customer can activate the same key on a new machine without contacting you. A computer that never reaches the internet can activate with files instead: offlineRequest writes a request file, you or the customer upload it at /offline on the server, and activateOffline loads the license file.

What happens when the computer is offline

Time since the last check-inStateThe app
0 to 72 hoursvalidWorks; verifies the lease locally
72 hours to 72 hours plus 7 daysgraceWorks; tell the user to connect before revalidateBy
After thatinvalid, reason revalidation_requiredStops until the next successful check-in
Clock moved back more than a dayclock_suspectPaid features stop until an online check

This is also why revocation is not instant: a revoked license keeps working on a computer that stays offline until its check-in deadline plus the grace period. Shorter check-in intervals in the policy shorten that window and need the network more often.

Limits

  • Licensing code in JavaScript can be patched by someone who controls the computer. Encryption per feature, Electron fuses and ASAR integrity raise the cost; they do not make it impossible.
  • Version 1 has node-locked licenses with seats. Floating or concurrent, named-user and metered licenses are not in it.
  • Payments are not built in. A payment provider calls the server's HMAC-signed order webhook to create a license.

Full documentation · Try it without signing up · How offline verification works

Sources