This guide assumes you already have a workspace. Each step gives the dashboard action and the underlying API.
1. Create a workspace
Workspaces are provisioned manually for now. Email sales@unionuser.com with your product name and expected volume; you will receive a sign-in link.
2. Create a product
Dashboard → Products → New. Desktop apps usually use the desktop type with device activation enabled. Keep the license prefix stable once set.
3. Create a plan
Choose the license model (subscription, perpetual or trial), device limit, grace period and entitlements. A common desktop setup:
{
"name": "Pro Yearly",
"license_model": "subscription",
"max_activations": 3,
"grace_days": 7,
"token_ttl_days": 7,
"entitlements": {
"publish": true,
"exports": 500
}
}
4. Issue a license
Dashboard → Licenses → New, pick the product and plan, and enter the customer email. Send the key to the user, or let Stripe issue it automatically.
5. Activate in the client
const BASE = 'https://keygate.unionuser.com';
async function activate(licenseKey, deviceId) {
const res = await fetch(BASE + '/api/v1/license/activate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
license_key: licenseKey,
identifier: deviceId,
identifier_type: 'device',
}),
});
if (!res.ok) throw new Error('activation failed: ' + res.status);
const body = await res.json();
return body.data.token; // store it locally
}
6. Verify offline
Pin the public key in the client. The token is base64url(payload).base64url(sig), and the signature covers the payload’s base64 string.
import { verify, createPublicKey } from 'node:crypto';
const RAW = Buffer.from('bf17435ee7b937b3592808941683c21d37c4e124d95d4e8fdd86fe775dd23b11', 'hex');
const SPKI = Buffer.concat([
Buffer.from('302a300506032b6570032100', 'hex'),
RAW,
]);
const publicKey = createPublicKey({ key: SPKI, format: 'der', type: 'spki' });
export function verifyToken(token) {
const [payload, sig] = token.split('.');
const ok = verify(
null,
Buffer.from(payload, 'ascii'),
publicKey,
Buffer.from(sig, 'base64url'),
);
if (!ok) throw new Error('INVALID_SIGNATURE');
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
if (claims.exp * 1000 < Date.now()) throw new Error('TOKEN_EXPIRED');
return claims;
}
Common pitfalls
- Unstable device id: avoid MAC addresses and hostnames. Use MachineGuid, IOPlatformUUID or machine-id, then hash it.
- Confusing exp with expiry: exp is the next check-in; vun is the license end.
- Clock tampering: offline checks are affected; pair with a locally recorded monotonic time.
- Reinstalls consume slots: the machine id can change; release the old device in the portal.