The CLV1 protocol is now publicRead the spec
Browse the library

Documentation libraryReleases

Signed releases and updates

Releases get each new build to the people who hold a license: the server hashes and signs every file, your software receives a signed manifest with short-lived download links, and you get the levers to force updates, block a bad build, pause sign-ins for maintenance and spot builds you never shipped.

10 min read

Before you start

  • Everything happens in Licensing & releases › Releases for each app.
  • Owners and Admins can create, publish and block releases and change release settings. Members can view releases and download files.
  • Release storage is shared across the whole workspace: every file of every app counts. By default Basic has 500 MB and Premium has 20,480 MB; the figure for your workspace is in Account & organization › Plan & usage and on the page's Storage (MB) meter. Whatever the plan, a single file can be at most 2 GB.

Creating a release

  1. Open Licensing & releases › Releases and click New release.
  2. Enter the Version in semver form: three numbers, optionally with a pre-release tag, such as 1.2.0 or 1.3.0-beta.1. No v prefix.
  3. Pick a Channel: stable (the default), beta, canary, plus any channel an earlier release already uses.
  4. Write the Release notes (optional, up to 20,000 characters). They appear on the release card and travel to your software inside the manifest.
  5. Drop files on Drop files here or click to choose, then check the Platform of each one.
  6. Click Save draft to keep it for review, or Publish to ship it right away.
PlatformThe dashboard picks it when the file name
win-x64ends in .exe, .msi or .msix
mac-arm64ends in .dmg, or contains arm64 together with mac or darwin
linux-x64ends in .AppImage, .deb, .rpm or .tar.gz
anymatches none of the above
win-arm64, mac-x64, linux-arm64never guessed, pick them yourself

If the guessed platform is already taken by another file, the dashboard assigns the first free platform instead, so review the dropdowns before you submit.

Rules to keep in mind:

  • A version can exist only once per channel. The same version number on two different channels is fine.
  • A release holds one file per platform; if two files share a platform, the later upload replaces the earlier one. File names are up to 200 characters with no / or \. Empty files are refused.
  • As a file uploads, the server computes its sha256 and size and signs them with the app's key. The release card lists the platform, file name, size and the first 10 characters of the sha256, with a button to copy the full hash.

When storage runs out

When storage is full, the server answers plan_limit. When a file is bigger than the room left, the upload stops with too_large. If an upload fails halfway, the draft that was already created stays in the list with whatever files made it. Click Delete draft and start again: the same version on the same channel is reported as a duplicate until the old draft is gone.

Lifecycle: draft, published, blocked

StatusYour customers' softwareWhat you can do
DraftCannot see or download itPublish (needs at least one file), Delete draft
PublishedGets it from checkUpdates() when it is the highest version on the channelBlock this build
BlockedMachines running it are refused at sign-in, download links stop workingUnblock
  • The page lists releases by version number, highest first.
  • Publish on a draft card stays disabled until the release has a file. Delete draft (the trash icon) deletes at once, with no confirmation.
  • Once published, the files and the channel are locked.
  • The "latest" release of a channel is the published one with the highest version number, not the most recent one. Publishing 1.1.5 after 1.2.0 does not make 1.1.5 the update.
  • A pre-release ranks below the final release with the same numbers: 1.3.0-beta.1 is lower than 1.3.0.

Blocking a bad build

  1. On the card of a published release, click Block this build.
  2. Type a reason under Why block this build (optional, up to 300 characters) and confirm.

From then on:

  • A machine whose appVersion matches this version number, on any channel, or whose buildHash matches the sha256 of one of this release's files, is refused at sign-in with build_blocked.
  • The release drops out of the manifest; checkUpdates() returns the highest published release left on the channel.
  • Every download link for its files answers 404, including links handed out a moment ago.
  • Open sessions are not cut. The block applies at the next sign-in.

A blocked card gets a red border and shows the reason. Click Unblock to put the release back to Published.

The Seller API can also list, publish and block releases with the releases:read and releases:write scopes; creating a release and uploading files is dashboard-only for now. See the Seller API reference.

Release channels

The channel list in the dialog holds stable, beta, canary and any channel that already has a release. Which channels a license receives is decided by its license plan. By default every plan receives stable only, and the dashboard has no setting to change that list yet. When your software asks for a channel its plan does not allow, the server falls back to the first channel the plan does allow. In practice checkUpdates() therefore always returns the stable release today; beta and canary are still useful for preparing and storing test builds.

Minimum version (forced update)

  1. In the Minimum version panel, find the channel's row.
  2. Type a version such as 1.2.0 and click Set.
  3. To remove the minimum, clear the field and click Set.

How it behaves:

  • On every activate, trial, register and login, the server compares the appVersion the SDK sends with the minimum of the stable channel. Older builds are refused with HTTP 426 and the code update_required.
  • If the SDK sends no appVersion, this check is skipped, so always declare the version in your build.
  • The minimum of any other channel does not block sign-ins; it ships in that channel's manifest as minVersion for your software to act on.
  • Open sessions carry on until their next sign-in. The change takes effect at once, and other API servers pick it up within 30 seconds.

Maintenance and notices

  • Turn on Maintenance mode to refuse every new sign-in with HTTP 503 and the code maintenance. Message during maintenance (up to 300 characters, saved when you leave the field) is what your users see. Open sessions are not cut.
  • In-app notice (up to 500 characters) is one short line returned as app.notice when your software calls init(). It is the same value as Message to users in Integration › App settings.

The order of the sign-in checks and how to show each code are covered in Sign-in modes and trials.

Unknown build check

When the SDK sends a buildHash (the sha256 of the running file, 64 lowercase hex characters), the server looks it up among the files you uploaded for the app:

  • It matches a file of a Blocked release: refused with build_blocked.
  • It matches any other file: let through.
  • It matches nothing, and the app has at least one Published release: this is an unknown build. The server records an Unknown build event in Protect 24/7, which adds to the risk score.

In Advanced Protection, turn on Build Integrity Verification (the Code Protection group) in App security › Protection, tab Methods and pick a mode:

  • Warn only: the sign-in goes through and you follow the events in Protect 24/7.
  • Block: the sign-in is refused with HTTP 403 and the code unknown_build.

In your software: the JavaScript SDK

Declaring the version and hash

import { ClavisClient, ClavisError } from "@clavisauh/sdk";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";

const APP_VERSION = "1.2.0";
// sha256 of the running file itself: the same file you uploaded to the release
const buildHash = createHash("sha256").update(readFileSync(process.execPath)).digest("hex");

const clavis = new ClavisClient({
  baseUrl: "https://api.example.com",
  appId: "app_…",
  appPublicKey,
  device,
  appVersion: APP_VERSION,
  buildHash,
});

The SDK sends appVersion and buildHash with every sign-in, and init() sends them too.

init() at startup

const { app } = await clavis.init();
if (app.maintenance) return showMaintenance(app.maintenance.message);
if (app.update?.required) return showUpdateScreen(app.update.minVersion, app.downloadUrl);
if (app.update?.latest) showBanner(`Version ${app.update.latest} is available`);
if (app.notice) showNotice(app.notice);
FieldMeaning
app.versionThe highest published stable release; with none, the Current version from App settings
app.minVersionThe stable minimum version, or null
app.update{ required: true, reason, minVersion } when the running build must update (update_required) or its version is blocked (build_blocked); { required: false, latest } when a newer stable release exists; otherwise null
app.maintenance{ message } during maintenance, otherwise null
app.notice, app.forceUpdate, app.downloadUrlThe values you set in the dashboard

init() only looks at the version number; the buildHash check happens at sign-in.

checkUpdates(): the signed manifest

checkUpdates({ channel?, platform? }) needs a live session (after activate, login and so on), otherwise it fails with session_required or session_expired; the license must also still be usable. It returns null when the channel has no published release. With platform, the manifest keeps only that platform's file and any any file.

The SDK hands you the manifest only when its signature checks out against an app key this build trusts (picked by kid, see Signing keys and rotation), the app id matches and it has not expired. A manifest is valid for 10 minutes from issue. If anything fails, the SDK throws a ClavisVerificationError with the code invalid_manifest.

The manifest carries app, channel, version, notes, publishedAt, minVersion, files (each with platform, fileName, sizeBytes, sha256, url), issuedAt, expiresAt and kid. It always describes the highest release on the channel, even when that equals the running version, so compare versions yourself.

download(): fetch and verify the sha256

const manifest = await clavis.checkUpdates({ platform: "win-x64" });
// isNewer is your own semver comparison
if (manifest && isNewer(manifest.version, APP_VERSION)) {
  const file = manifest.files[0];
  try {
    const bytes = await clavis.download(file);
    await writeFile(join(tmpdir(), file.fileName), bytes);
  } catch (err) {
    if (err instanceof ClavisError) showError(err.code); // download_failed, checksum_mismatch
  }
}

download() loads the whole file into memory, hashes it and compares the result with the manifest. A single changed byte gives checksum_mismatch; an HTTP error gives download_failed with the status code.

Each url in a manifest lives for 5 minutes and is bound to the session that asked for it.

SituationHTTPCode
The link is older than 5 minutes410link_expired
The link was tampered with403invalid_link
The session logged out, expired or was revoked401session_expired
The release is blocked or the file is gone404not_found
  • The manifest is valid for 10 minutes but its links for only 5. If the user waits before updating, call checkUpdates() again for fresh links.
  • Each successful download by your software adds to the download count on the release card and records a release.download event in App security › Activity log. Each checkUpdates() call that returns a manifest also records release.check.
  • The Download button in the dashboard gives you a 5-minute link that is not tied to a session and does not count as a download.

Who runs which version

The Versions in use panel groups the machines seen in the last 7 days by the version they report, up to 6 rows; machines that report no version fall under "unknown". Each release card also shows the share of machines "running it" and its download count. This is where you decide when to raise the Minimum version without leaving too many customers behind.

How can we help?

Frequently asked

What is ClavisAUH?

A licensing and authentication service for desktop software: license keys, device binding, end-user sign-in and signed responses. All run from one dashboard

Why can't a cracker fake your server?

The SDK only trusts answers signed with your app's private key. That key never leaves our servers, so a fake server can't sign and gets rejected

What ends up in my binary?

Your app id and public key. Nothing secret, because each device makes its own key pair on first run

Does it work offline?

Yes. Every plan gets a signed offline token for 7 days, and your app checks it locally. Premium lets you choose how long. A revoked license can keep working offline for that time, unless Online-Only Mode is on

Which languages are supported?

JavaScript and TypeScript today. C++, C# and Python are next, on the same open protocol

Is Basic really free?

Yes. No time limit, no card. Move to Premium when you need more

Get in touch