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.
Global variables live on the product page (a message of the day, a download link, feature settings) and your app reads them with variable(name). Like KeyAuth's "authenticated" flag, each one is either licensed (sent only to a device with a valid, activated license) or public (sent to any copy of your app, even before sign-in, which suits version checks and announcements).
User variables are stored on a single license. Your app reads them with user_variables() and saves its own with set_user_variable(name, value). Values you mark read-only on the license page can be read but not changed by the app. Up to 50 per license, 1,000 characters each.
news = atlas.variable("news") # public: works before sign-in
atlas.license(key)
motd = atlas.variable("motd") # licensed
atlas.set_user_variable("theme", "dark")
tier = atlas.user_variable("tier") # e.g. read-only, set by youNot for secrets
Upload a file (up to 10 MB), or, like KeyAuth, give Atlas a direct download link (up to 50 MB) that it fetches for each download, so you can keep files on your own storage. Files are licensed by default; mark one public to let anyone download it. Your app calls download(file_id), and the SDK checks the bytes against a SHA-256 inside the signed response, so a tampered download is rejected.
data = atlas.download("file_…") # bytesLinks must be public http or https URLs. Atlas refuses links that point at private or internal networks, including after redirects.
Each product has a blacklist of IPs (single addresses or CIDR ranges), device IDs and countries. A blacklisted caller gets a signed blacklisted result from every call (license checks, sign-in, variables, files), and activations and offline leases are refused. Apps can check up front with is_blacklisted().
The whitelist holds IPs and devices that are never blocked, even when a blacklist rule matches them. For example, you can block a country but allow one tester.
Ban on a license page works like banning a KeyAuth user: it suspends the license, disables its account, and blacklists every device it was activated on, so a new key won't work on those machines either.
Country rules
CF-IPCountry, Vercel or CloudFront headers). Without one, country rules never match.log(message) sends a line to the product's Logs tab, tagged with the license, account and device that sent it. It's useful for crash reports and tracking down abuse. Up to 2,000 characters, 60 per minute per IP.
Every 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