Guides
A complete client-side flow: ask for a key, activate the device, check the license on launch, and react to the result.
The device ID is how Atlas tells machines apart. It should stay the same across restarts and app updates, but differ between machines. Good options are an OS machine ID (for example from node-machine-id) or a random ID you generate on first launch and store. Avoid hostnames and IP addresses, which change.
Optionally send a fingerprint too, such as a hash of hardware details. Later checks from the same device ID with a different fingerprint fail with device_mismatch, which stops a copied ID from working elsewhere.
import { AtlasRuntime, AtlasError } from "@atlas/sdk";
const atlas = new AtlasRuntime({ publishableKey: PUBLISHABLE_KEY });
async function activate(licenseKey: string) {
try {
const { data } = await atlas.activate({
license_key: licenseKey,
product_id: PRODUCT_ID,
device: { id: machineId, name: os.hostname(), platform: process.platform },
app_version: APP_VERSION,
});
await keychain.save("license", licenseKey);
return data.license;
} catch (error) {
if (error instanceof AtlasError && error.code === "activation_limit_reached") {
return showMessage("This license is already in use on its maximum number of devices.");
}
throw error;
}
}Activating a device that's already active is safe and doesn't use up another slot, so you can call it again if you're unsure.
Validation doesn't throw for license problems. It always returns valid plus a code explaining why.
const { data } = await atlas.validate({
license_key: storedKey,
product_id: PRODUCT_ID,
device: { id: machineId },
});
if (data.valid) {
if (data.in_grace) showBanner("Your license has expired. Renew soon to keep access.");
enableFeatures(data.license!.entitlements);
} else {
switch (data.code) {
case "license_expired": return showRenewScreen();
case "license_suspended": return showMessage("Your license is paused. Contact support.");
case "not_activated": return showActivationScreen();
default: return showMessage("This license can't be used.");
}
}Every possible code is listed in Errors. If the request itself fails because there's no internet, fall back to offline mode or let the user continue for a short while.
Add a Deactivate this device option in your app's settings. It frees the slot immediately so the customer can activate somewhere else.
await atlas.deactivate({
license_key: storedKey,
product_id: PRODUCT_ID,
device_id: machineId,
});Let customers sign in with a username and password instead of typing their key. They create the account once from your app with their license key; after that, signing in activates the device and validates the license in one call.
atlas.register("ines", "correct-horse", "ATLAS-7K2M-…") # once
result = atlas.login("ines", "correct-horse") # every launch
if not result.valid:
show(result.message) # e.g. "Incorrect username or password."Each account belongs to one license, and usernames are unique per product. You can reset passwords, disable or delete accounts on the product's page in the dashboard. Sign-in attempts are rate limited per IP.
Store values on the product page (a message of the day, a download URL, feature settings) and read them at runtime. Atlas only returns them to a device holding a valid, activated license, inside a signed response.
motd = atlas.variable("motd") # after license() or login()Not for secrets
Upload files of up to 10 MB on the product page, then download them from your app by ID. Atlas checks the license first, and the SDK checks the bytes against a SHA-256 inside the signed response, so a tampered download is rejected.
data = atlas.download("file_…") # bytesEvery result includes a message you can show the user as-is, such as "This license has expired. Renew it to keep using the app.", alongside the machine-readable code.
Every runtime response is signed by Atlas, so your app can confirm it really came from Atlas and wasn't edited along the way. It's optional, but recommended for paid apps.
atlas.signingKey() (or GET /v1/runtime/key) and bundle it into your app.kid matches the key you bundled.signature (base64) against data serialized as JSON with keys sorted alphabetically at every level and no whitespace.data.expires_at has passed. Responses are valid for five minutes.nonce with each request and check it comes back in data.nonce. This stops an old response being replayed.For offline leases, the SDK's verifyOfflineLease() does all of this for you.
Each organization has its own signing key. To replace it, select Rotate key under API keys. The old key keeps verifying as previous, and GET /v1/runtime/key lists both in keys, so apps you've already shipped keep working. Retire the old key once every version you support trusts the new one.
Know the limits