SDK integration
Node.js SDK integration
Add Keyright licensing to a Node.js app: install the keyright package, verify licenses offline, activate online, gate features on entitlements, and handle trials and air-gapped machines — failing closed by design.
This is the Node.js counterpart to the .NET SDK walkthrough — same license and lease formats, same public key, same fail-closed guarantees, but for a Node runtime. The keyright package verifies licenses offline against your embedded public key and, when you want seat enforcement and revocation, activates online to cache a short-lived signed lease. Every code block below uses the real SDK surface; copy them as-is.
Because all the Keyright SDKs verify the same wire formats, a mixed-language product line can share one Keyright tenant and one public key. If your product also ships a .NET, Python, or Java component, they can all validate the very same keys and leases.
The model, in one paragraph
You issue license keys server-side in Keyright — each tenant signs them with an RSA private key that never leaves the server. Your Node app embeds only the matching public key and verifies licenses offline, so a license check needs no network. For seat enforcement and revocation the app also activates online: it exchanges a license key for a short-lived signed lease bound to the machine, then keeps working offline until that lease expires. Anything that doesn’t check out — bad signature, wrong product, expiry, revocation, a rolled-back clock — resolves to the Free edition instead of throwing. It fails closed.
Vendor setup (do this once)
The dashboard steps are language-agnostic and identical to the .NET guide, so this page keeps them short:
- Create your product and note its slug (e.g.
acme-app) — that string is what your client sends asproduct. - Copy your public key — the base64
SubjectPublicKeyInfofrom the dashboard’s Integration tab. It ships inside your app and is not a secret; it can only verify signatures, never mint them. - Define your tiers & entitlements — each tier carries a seat count and the named flags/limits baked into every license of that tier.
- Issue keys — from the dashboard, your billing webhook, or the admin API.
For the full version of these steps (dashboard screenshots, curl calls, the tier/entitlement model, and how keys reach customers), see Getting started and steps 1–4 and 8 of the .NET SDK walkthrough. The rest of this page is all Node client code.
Install the SDK
keyright is on npm (>= 1.1.4). It’s a CommonJS package with zero dependencies — signature verification runs on Node’s built-in crypto.
npm install keyright
const { KeyrightClient } = require('keyright');
Initialize the client
Construct one client at startup with KeyrightClient.initialize. The options are a plain object with camelCase keys — there’s no options class to new up:
const { KeyrightClient } = require('keyright');
const license = KeyrightClient.initialize({
product: 'acme-app', // must match the product slug you issue keys for
publicKeyBase64: 'MIIBIjANBgkq...', // the base64 public key from vendor setup
serviceUrl: 'https://keyright.delta1labs.com', // omit if you ship offline license files only
// Optional: keep an old key valid during a rotation window
// additionalPublicKeysBase64: ['<previous public key>'],
});
Only product and publicKeyBase64 are required; initialize throws immediately if publicKeyBase64 is missing, and parses the key(s) up front. serviceUrl is needed only for online activate/deactivate.
Beyond those, the client resolves a license from several sources in precedence order (highest first):
| Option | What it is |
|---|---|
licenseString | A license JSON string you supply directly. |
licenseFilePath | A path to a license file. |
envVarName | Name of an env var holding a license-file path. Set it to 'KEYRIGHT_LICENSE' to honour that convention — it is not read unless you name it. |
configLicensePath | A config-supplied license-file path. |
| (default) | The OS app-data path …/Keyright/<product>/license.json. |
| (lease) | The cached activation lease — written for you by activate. |
You usually set none of these — activation writes the lease cache automatically. Other optional knobs: revocationListJson / revocationListPath (ship a signed revocation list for offline builds), nodeLockTolerance (default 1), clockTamperToleranceHours (default 24), additionalPublicKeysBase64, and machineComponents (customize what identifies a machine).
Gate features offline
client.validate() is synchronous. It resolves the best license from the sources above, verifies the RSA signature, product match, node-lock, expiry, the optional shipped revocation list, and trial/clock-tamper state — all offline — and it never throws. It returns { info, source }: destructure it. On any failure info is a LicenseInfo in the free edition carrying the reason, and source tells you which source won (e.g. 'lease', 'default', 'none').
const { info, source } = license.validate();
if (info.isPaid) { // true for any edition above Free
// unlock paid features
}
if (info.edition === 'enterprise') {
// unlock enterprise-only features
}
LicenseInfo exposes camelCase fields and getters: status, isValid, isPaid, isTrial, edition, licensee, message, expiryUtc (a Date or null), daysRemaining (a getter, null when there’s no expiry), statusBadge (e.g. "Enterprise Trial"), and entitlements. status is one of the lowercase LicenseStatus strings: 'valid', 'no_license', 'malformed', 'signature_invalid', 'expired', 'machine_mismatch', 'revoked', 'clock_tampered'. edition is 'free', 'licensed', or 'enterprise'.
Gate individual features on entitlements rather than on the edition, so changing a tier’s template doesn’t mean shipping new code. info.entitlements is an EntitlementSet with isEnabled(name) and getLimit(name, fallback), and both fail closed:
const { info } = license.validate();
// Boolean flag — missing/unparseable reads as disabled
if (info.entitlements.isEnabled('export')) {
showExportCommand();
}
// Numeric limit — pass the fail-closed fallback yourself; 'unlimited' reads as a huge number
const maxProjects = info.entitlements.getLimit('max-projects', 1);
if (currentProjectCount >= maxProjects) {
promptToUpgrade();
}
isEnabled treats 'true', '1', 'yes', 'enabled' (case-insensitive) as on; anything else, or a missing flag, is off. getLimit returns your fallback for a missing or non-integer value, and a maximum-safe integer for the literal 'unlimited'. Entitlement names are case-insensitive.
The client also offers one-shot convenience wrappers that validate on the spot — license.isEnabled('export'), license.getLimit('max-projects', 1), license.entitlements(), and license.edition() — but if you check several entitlements at once, call validate() once and reuse info.entitlements to avoid repeated verification work.
To find out why a check failed (for a “Register” dialog or diagnostics), read info.status and the human-facing info.message.
Activate online with a license key
When a customer enters a key, call await client.activate(key). It POSTs the key plus a stable machine id to <serviceUrl>/v1/activate; the service consumes a seat and returns a short-lived signed lease bound to that machine. The SDK verifies the lease against your embedded public key and caches it locally, so every later validate() succeeds with no network until the lease’s grace window elapses.
activate returns a LicenseInfo and does not throw for the ordinary failure paths (bad key, seat limit, offline, revoked) — like validate(), it returns a fail-closed LicenseInfo. Inspect the result:
const info = await license.activate(customerEnteredKey);
if (info.isValid && info.isPaid) {
// Activated. The lease is cached; the app now works offline until it expires.
showLicensedUi(info.statusBadge); // e.g. "Enterprise" or "Enterprise Trial"
} else {
// Surface info.message; the app stays in Free mode.
showActivationError(info.message); // "All seats for this license are in use.", etc.
}
- Seats are enforced server-side. Activating more machines than the license allows returns a seat-limit result (
message: "All seats for this license are in use.") and no lease. Re-activating a machine that’s already bound is idempotent — no extra seat is consumed. - Offline grace. If the service is unreachable,
activatefalls back to any still-valid cached lease and returns it, so a brief outage doesn’t lock the user out. Only if there’s no valid cached lease does it return a Free info with the connection error inmessage. - Revocation takes effect on the next refresh.
activate throws only for programmer errors: a missing serviceUrl or an empty key.
The machine id it sends is MachineFingerprint.current().toBoundString() — a stable per-device fingerprint. nodeLockTolerance (default 1) lets a swapped NIC or disk slide without locking the user out.
Deactivate — free a seat
To release this machine’s seat (before decommissioning a box, or so the customer can move the license), call await client.deactivate(key). It POSTs { key, machineId, product } to <serviceUrl>/v1/free-seat and returns the status string — 'ok' or 'not_found'. On 'ok' it also deletes the locally cached lease, so the next validate() drops back to Free.
Unlike activate, deactivate has no offline fallback: if the HTTP call fails it throws. Wrap it:
try {
const status = await license.deactivate(customerEnteredKey);
if (status === 'ok') {
// Seat released and local lease cleared.
} else {
// 'not_found' — nothing was bound for this key/machine.
}
} catch (err) {
// Could not reach the issuing service — try again when back online.
}
Offline & air-gapped machines
For a machine that can never reach the service, an operator signs an offline lease for its machine id (dashboard Licenses → offline lease, or POST /admin/licenses/{id}/offline-lease) and delivers the JSON by file. The machine id to give the operator is exactly what the SDK derives:
const { MachineFingerprint } = require('keyright');
console.log(MachineFingerprint.current().toBoundString());
Import the signed lease with client.importOfflineLease(json). It verifies the lease through the same code path a cached lease uses (signature, product, machine, expiry), caches it so later offline validate() calls succeed, and returns a LicenseInfo. Unlike activate, it throws if the lease is invalid — handle it:
const fs = require('fs');
try {
const info = license.importOfflineLease(fs.readFileSync('acme.lease.json', 'utf8'));
// info.isValid === true; the lease is now cached on this machine.
} catch (err) {
// wrong signature / product / machine, or expired
showActivationError('This offline lease could not be verified: ' + err.message);
}
It accepts either a JSON string or an already-parsed object.
Trials
A trial key activates through the exact same activate path as a paid one, and supersedes seamlessly when the customer later activates a paid key. Read the trial state straight off LicenseInfo:
const { info } = license.validate();
if (info.isTrial) {
const left = info.daysRemaining; // e.g. 27, or null if no expiry
showTrialBadge(info.statusBadge, left); // statusBadge -> "Enterprise Trial"
// info.expiryUtc is a Date you can format for a "trial ends on…" line
}
A trial’s duration is counted from first activation on the machine: the SDK persists a small local state file and, once the trial window elapses, validate() returns a Free info with status: 'expired'. If the system clock is moved backward beyond clockTamperToleranceHours (default 24h) on a trial or subscription, validate() returns status: 'clock_tampered'; it recovers on the next call once the correct time is set. Perpetual licenses are never subject to either check. Full flow: Self-service free trials.
A minimal end-to-end example
const { KeyrightClient } = require('keyright');
const license = KeyrightClient.initialize({
product: 'acme-app',
publicKeyBase64: 'MIIBIjANBgkq...',
serviceUrl: 'https://keyright.delta1labs.com',
});
async function main() {
// 1. Offline check on startup — no network needed.
let { info } = license.validate();
// 2. If unlicensed and the user pasted a key, activate online.
if (!info.isPaid && process.env.LICENSE_KEY) {
info = await license.activate(process.env.LICENSE_KEY);
}
// 3. Gate features on entitlements, failing closed.
if (info.isPaid && info.entitlements.isEnabled('export')) {
console.log('Export enabled for', info.licensee, '(' + info.statusBadge + ')');
} else {
console.log('Running in Free mode:', info.message || 'no paid license');
}
}
main();
Troubleshooting
- Activation says the key wasn’t recognized. The most common cause is a product mismatch: the key was issued for a different product (or a different tenant) than the
productslug your client is configured with. The server scopes activation by product, so a valid key foracme-appwon’t activate a client initialized withproduct: 'other-app'. Confirm the slug matches the product you issued the key under. - Everything reads as Free. That’s the design — the SDK fails closed on any verification problem instead of throwing. Read
info.status('no_license','signature_invalid','expired','machine_mismatch','revoked','clock_tampered') andinfo.messageto see which one. A'signature_invalid'almost always means the embeddedpublicKeyBase64doesn’t match the tenant that signed the key. - A time-limited license suddenly won’t validate. If the system clock moved backward beyond
clockTamperToleranceHours(default 24h) on a trial or subscription, the status is'clock_tampered'. It doesn’t stick — set the correct time and validation recovers on the next call. Perpetual licenses are never subject to this check. deactivatethrows. It has no offline fallback by design — a failed HTTP call throws rather than silently succeeding. Only when it returns'ok'is the seat freed and the local lease cleared; retry when connectivity returns.importOfflineLeasethrows. The lease failed verification (wrong signature/product/machine, or expired) — confirm the operator signed it for the machine id fromMachineFingerprint.current().toBoundString()on this box.
See also
- Getting started — the condensed vendor path and go-live checklist.
- Activation lifecycle — how keys, leases, seats, and refreshes fit together.
- Security & leases — the signing model, lease format, and offline-grace design.
- HTTP API reference — the full endpoint map behind
activateanddeactivate. - Keyright samples on GitHub — runnable client examples.