Most products people call "an app" are really several programs in a trench coat. The thing a customer buys as Acme Suite might be a WPF desktop app in C#, a dotnet-and-npm command-line tool in Node, a data-crunching library in Python, and a long-running service on the JVM — four runtimes, one purchase. The question that decides whether licensing that product is pleasant or miserable is simple: does the customer get one license that every component honours, or four separate activations that drift out of sync the first time anyone touches the pricing page?
Keyright's answer is that all four SDKs — .NET, Node.js, Python and Java — verify the exact same signed license and lease formats against the same public key. One tenant signs; four runtimes verify; the customer sees one key. This post shows that flow in all four languages at once, and then spends its time on the two things that are genuinely shared (the signature model and the machine fingerprint) and the two that are genuinely per-component (the lease cache and the resolution path), because that is where a polyglot rollout succeeds or trips.
The shared model, once
The model is identical no matter which runtime is asking. You issue a license key server-side; your tenant signs it with an RSA private key that never leaves the server. Every component of your product embeds only the matching public key — a base64 SubjectPublicKeyInfo string that is not a secret and can only verify, never mint. A routine check verifies the key offline against that public key: signature, product slug, node-lock, expiry, revocation, clock-tamper state. Anything that does not check out resolves to the Free edition instead of throwing. It fails closed, everywhere, the same way.
The same flow in four languages
Here is the whole client story — initialize once, validate offline, gate a feature on an entitlement — in each SDK. Note that nothing but the language changes: the same product slug, the same public key, the same export entitlement.
.NET — validate() returns a LicenseInfo; activation is async:
using Keyright.Client;
static readonly KeyrightClient License = KeyrightClient.Initialize(new KeyrightOptions
{
Product = "acme-suite", // one slug for the whole product line
PublicKeyBase64 = "MIIBIjANBgkq...", // the same public key in every component
ServiceUrl = "https://licensing.acme.example", // your issuing service
});
LicenseInfo info = License.Validate(); // offline, never throws, fails closed to Free
if (info.IsPaid && License.IsEnabled("export"))
EnableExport();
Node.js — validate() returns { info, source }; activate is async:
const { KeyrightClient } = require('keyright');
const license = KeyrightClient.initialize({
product: 'acme-suite',
publicKeyBase64: 'MIIBIjANBgkq...',
serviceUrl: 'https://licensing.acme.example',
});
const { info } = license.validate(); // synchronous, fails closed to free
if (info.isPaid && info.entitlements.isEnabled('export'))
enableExport();
Python — validate() returns a (info, source) tuple, with validate_info() for just the info:
from keyright import KeyrightClient, KeyrightOptions
license = KeyrightClient.initialize(KeyrightOptions(
product="acme-suite",
public_key_base64="MIIBIjANBgkq...",
service_url="https://licensing.acme.example",
))
info = license.validate_info() # fails closed to Free
if info.is_paid and info.entitlements.is_enabled("export"):
enable_export()
Java — validate() returns a Result with public info/source fields; activation is synchronous:
import com.delta1labs.keyright.*;
KeyrightClient client = KeyrightClient.initialize(
new KeyrightOptions()
.product("acme-suite")
.publicKeyBase64("MIIBIjANBgkq...")
.serviceUrl("https://licensing.acme.example"));
LicenseInfo info = client.validateInfo(); // fails closed to Free
if (info.isPaid() && info.entitlements.isEnabled("export"))
enableExport();
Four runtimes, one behaviour. The only differences are idiom — PascalCase versus camelCase versus snake_case, a tuple versus an object versus an out-parameter for the resolution source, async in C# and Node against synchronous calls in Python and Java. The RSA verification, the fail-closed-to-Free guarantee, and the entitlement semantics are byte-for-byte the same.
The payoff: one seat covers the whole box
The feature that makes this more than a convenience is node-locking. Each SDK derives a machine fingerprint — anchored on the OS machine GUID plus soft signals like hostname and OS — and that fingerprint is byte-compatible across all four SDKs. The Java SDK's docs put it plainly: its fingerprint is "byte-compatible with the other SDKs." The practical consequence is large.
When any component activates, it sends machineId = MachineFingerprint.current().toBoundString() (the same string in every language) and the server binds a seat to (key, machineId). Because the machine id is identical across the .NET app, the Node CLI, the Python tool and the JVM service on one box, and because re-activating an already-bound machine is idempotent, all four components on that machine resolve to the same seat. A three-seat license is three machines, not twelve component-activations. Your customer installs the suite on three laptops and is fully licensed — exactly the mental model they already have, and exactly what a per-component counter would have gotten wrong.
To get a machine's id for a support call or an air-gapped activation, every SDK offers the same thing in its own idiom — MachineFingerprint.current().toBoundString() in .NET and Java, .to_bound_string() in Python, MachineFingerprint.current().toBoundString() in Node — and a short KR-XXXX-XXXX display form for humans.
What is genuinely per-component
Two things do not cross component boundaries automatically, and a clean rollout plans for both.
The lease cache is local to each SDK. When a component activates, it caches the signed lease at its own OS app-data path (.../Keyright/<product>/lease.json and a sibling lease file). The .NET app's cache is not the Node CLI's cache. So in the default setup each component activates once — the first does a real round-trip and consumes the seat; every later component on the same machine re-activates, matches the existing seat for free, and caches its own lease. That is harmless but it is N activations. If you would rather activate once for the whole suite, you have two clean options:
- Point every component at one license file through the shared environment variable. All four SDKs honour
KEYRIGHT_LICENSEas a resolution source (Node reads it when you setenvVarName: 'KEYRIGHT_LICENSE'; the others default to it), so an installer that drops an activated license file and exportsKEYRIGHT_LICENSElicenses the whole bundle with no per-component round-trip. - Or let one component own activation and write its lease to a path the others read via
configLicensePath/LicenseFilePath.
The resolution order is each SDK's own. Every SDK looks for a license in the same precedence — an explicit license string, then an explicit file path, then the KEYRIGHT_LICENSE env var, then a config path, then the OS app-data default, then the cached lease — but it walks that list in its process, against its configured paths. That is a feature: a container-hosted JVM service can be fed its license by KEYRIGHT_LICENSE from its secret store while the desktop C# app on the same customer's laptop uses an interactive activation and its cached lease, and both verify the identical signed document.
Rotating the key across the whole line
Because there is one key pair behind the whole product, a signing-key rotation is one coordinated release rather than four independent scrambles. Every SDK exposes the same escape hatch: alongside the new public key you list the outgoing one, and licenses and leases signed by either keep validating through the transition.
// .NET
AdditionalPublicKeysBase64 = { "<previous public key>" },
// Node
additionalPublicKeysBase64: ['<previous public key>'],
# Python
additional_public_keys_base64=["<previous public key>"],
// Java (public field, no fluent setter)
opts.additionalPublicKeysBase64.add("<previous public key>");
Ship every component with the new key primary and the old key in the additional list, let the field drain as old leases expire, then drop the old key in a later release. The rotating your signing keys post covers the timing; the only polyglot wrinkle is that "ship a build" now means four builds carrying the same two keys.
The takeaway
A product line in four languages does not need four licensing systems — it needs one signed format and four verifiers of it, which is exactly what the Keyright SDKs are. The signature model is shared (one private key signs, one public key verifies, everywhere), the machine fingerprint is shared (byte-compatible, so one seat covers every component on a machine), and the entitlement semantics are shared (the same flags and limits, read fail-closed out of the same claim). What is local — the lease cache and the resolution path — is local by design, and the KEYRIGHT_LICENSE environment variable collapses it to a single activation when you want that. The customer buys one license; C#, JavaScript, Python and the JVM all honour it; and you maintain one tenant instead of four.
See the per-language guides for the full client surface — .NET, Node.js, Python and Java — and licensing containerized services for the component of the suite that scales horizontally.
Top comments (0)