The CLV1 protocol is now publicRead the spec
Browse the library

Documentation libraryGet started by language

Get started with Rust

This guide takes a Rust program from nothing to its first activated license, and on to the build you hand to customers: install the SDK, a few lines of code, a test run, the result on the dashboard, errors, and release.

5 min read

What you need

  • Rust 1.80 or later. The first build needs a network, for Cargo to fetch the crates the SDK uses (ed25519-dalek, ureq, serde…).
  • An app on the dashboard. Its Integration › Connect software page has the API address, the App ID, the public key and a Make a test key button. The API address ends in /api, with no /v1: https://<your ClavisAUH site>/api.

1. Install the SDK

Download clavisauh-rust-1.0.0.zip from Downloads, check its SHA-256 as that page shows, and unpack it in your crate's folder (a new program: cargo new your_app first; then unzip clavisauh-rust-1.0.0.zip, or Expand-Archive clavisauh-rust-1.0.0.zip . in PowerShell): it holds one folder, clavisauh-rust-1.0.0, with the SDK. Add it as a path dependency:

cargo add --path clavisauh-rust-1.0.0

Your Cargo.toml then has, under [dependencies]:

clavisauh = { version = "1.0.0", path = "clavisauh-rust-1.0.0" }

A project made with cargo new from Rust 1.85 on (edition 2024) picks crate versions that suit your compiler. In an older project, when Cargo says a crate needs a newer rustc, add this file, as the starter does:

# Resolve dependencies to versions that still build with the crate's rust-version (Cargo 1.84+ reads this; an older
# Cargo ignores it and uses Cargo.lock as committed).
[resolver]
incompatible-rust-versions = "fallback"

The SDK is not on crates.io yet; once it is, this guide will show that way too.

2. The minimal code

The blocks below are parts of one file; the whole file, with its imports and helpers, is in step 3.

Create the client once, when your software starts. The API address, App ID and public key are the three values on the Connect software page: the sample reads them from environment variables, your software writes them in its code. The device key lives in a file only the current user can read: sessions and offline tokens are bound to it, while the seat on the license follows the machine's hashed identifiers:

let app_key = var("CLAVIS_APP_KEY");
let mut options = Options::new(
    &var("CLAVIS_BASE_URL"),
    &var("CLAVIS_APP_ID"),
    &[&app_key],
    default_device(None),
);
// The same key every run: sessions and offline tokens stay with this machine.
options.device_key =
    Some(load_or_create_device_key(state.join("device.key")).expect("the device key file"));
options.app_version = Some("1.0.0".into());
let mut client = Client::new(options).expect("the app settings");

Activate the customer's key. Without a network, the offline token saved last time keeps the software running:

let grant = match client
    .init()
    .and_then(|_| client.activate(&var("CLAVIS_LICENSE")))
{
    Ok(grant) => Some(grant),
    Err(err) => {
        // No answer (network_error, timeout...): a token saved last time keeps the software working.
        let token = fs::read_to_string(&saved).unwrap_or_default();
        if !err.retryable() || token.is_empty() || client.verify_offline(Some(&token)).is_none()
        {
            fail(&err, &lang);
        }
        println!("Offline: using the license saved on this machine");
        None
    }
};

The public key only checks signatures, so embedding it in your software is safe. Never embed any secret key.

3. Run it

  1. On the Connect software page, select Make a test key. A TEST key expires after 24 hours and does not count against your limits.
  2. Save the whole file below as your project's src/main.rs (the starter, with each scenario as a binary of its own, has it as k1_key.rs and runs cargo run --bin k1_key), set the four settings and run it:
//! K1 · License key only: activate on this machine, keep the session alive, work offline for a while.
//!
//! Run: CLAVIS_BASE_URL=... CLAVIS_APP_ID=... CLAVIS_APP_KEY=... CLAVIS_LICENSE=... cargo run --release
//! (saved as src/main.rs in your crate; the starter project runs it with cargo run --bin k1_key)

use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::exit;

use clavisauh::{default_device, load_or_create_device_key, Client, Error, Options};

fn var(name: &str) -> String {
    env::var(name).unwrap_or_default()
}

/// Prints the sentence for the user and the code, and stops.
fn fail(err: &Error, lang: &str) -> ! {
    println!("{}", err.user_message(lang));
    println!("FAIL {}", err.code);
    exit(1)
}

fn main() {
    let lang = env::var("CLAVIS_LANG").unwrap_or_else(|_| "en".into());
    let state = PathBuf::from(env::var("CLAVIS_STATE_DIR").unwrap_or_else(|_| ".clavis".into()));
    let saved = state.join("offline.token");

    let app_key = var("CLAVIS_APP_KEY");
    let mut options = Options::new(
        &var("CLAVIS_BASE_URL"),
        &var("CLAVIS_APP_ID"),
        &[&app_key],
        default_device(None),
    );
    // The same key every run: sessions and offline tokens stay with this machine.
    options.device_key =
        Some(load_or_create_device_key(state.join("device.key")).expect("the device key file"));
    options.app_version = Some("1.0.0".into());
    let mut client = Client::new(options).expect("the app settings");

    let grant = match client
        .init()
        .and_then(|_| client.activate(&var("CLAVIS_LICENSE")))
    {
        Ok(grant) => Some(grant),
        Err(err) => {
            // No answer (network_error, timeout...): a token saved last time keeps the software working.
            let token = fs::read_to_string(&saved).unwrap_or_default();
            if !err.retryable() || token.is_empty() || client.verify_offline(Some(&token)).is_none()
            {
                fail(&err, &lang);
            }
            println!("Offline: using the license saved on this machine");
            None
        }
    };

    let Some(grant) = grant else {
        println!("OK k1 offline");
        return;
    };
    let until = grant.license.expires_at.as_deref().unwrap_or("forever");
    println!("License {} until {until}", grant.license.status);
    // Every few minutes while the software runs; sessions end after an hour without one.
    if let Err(err) = client.heartbeat() {
        fail(&err, &lang);
    }
    if let Some(token) = &client.offline_token {
        fs::write(&saved, token).expect("saving the offline token");
    }
    let _ = client.logout();
    println!("OK k1 {}", grant.license.status);
}
export CLAVIS_BASE_URL="<API address>"
export CLAVIS_APP_ID="<App ID>"
export CLAVIS_APP_KEY="<public key>"
export CLAVIS_LICENSE="<test key>"
cargo run --release

On Windows, in PowerShell:

$env:CLAVIS_BASE_URL = "<API address>"
$env:CLAVIS_APP_ID = "<App ID>"
$env:CLAVIS_APP_KEY = "<public key>"
$env:CLAVIS_LICENSE = "<test key>"
cargo run --release

The starter project (Download the starter (.zip) on the dashboard's Connect software page, once Rust is picked; it is not on the Downloads page) runs this scenario with ./run.sh <key> or .\run.cmd <key>.

It prints License active until <date>, then OK k1 active on the last line. Run it again on the same machine and it still succeeds without using another seat: the server knows the machine by its hashed identifiers (Devices, HWID and transfers). The device key, which signs the requests, stays in the .clavis folder under the folder you run it from (or in CLAVIS_STATE_DIR). A container started afresh for each run looks like a new machine every time, see Questions.

4. See it on the dashboard

  • Integration › Connect software: step 4 turns to Connected, with rust/1.0.0 and the machine's operating system.
  • Licensing & releases › Licenses: the key goes from Unused to Active.
  • End users › Devices & sessions: the machine that ran it, and the session it opened.

5. Handle errors

Every refusal is a clavisauh::Error with three things you need:

  • err.code: a code from the error codes table, such as invalid_credentials or device_limit.
  • err.user_message("en"): a sentence for the person using your software, in English or "vi", ready to show.
  • err.retryable(): true when no answer arrived (no network, too slow, a busy server); trying again later may work.

When err.verification is true, the answer did not carry your app's valid signature (code unsigned_response, invalid_response_signature, unknown_key or stale_response). Treat it as no answer at all; usually the API address or the public key in your software is wrong.

While your software runs, send a heartbeat regularly to keep the session, every 5 to 30 minutes or so: a session ends after 60 minutes without one.

// Every few minutes while the software runs; sessions end after an hour without one.
if let Err(err) = client.heartbeat() {
    fail(&err, &lang);
}

Save the latest offline token, so the software still opens next time without a network:

if let Some(token) = &client.offline_token {
    fs::write(&saved, token).expect("saving the offline token");
}

Offline tokens come on every plan and last 7 days by default; Premium chooses the length, and Online-Only Mode turns them off. See Offline licenses. To see every call while tracking a problem down, set options.debug = Some(Box::new(clavisauh::debug_stderr));: each call and its outcome go to stderr, keys show only their first and last four characters, and passwords never appear.

6. Package and release

  • Write the API address, App ID and public key in your code or in a config file shipped with the build. Customers' machines need no environment variables.
  • Put the state folder (device key, offline token) where the user can write, one per user: %APPDATA%\YourApp on Windows, ~/Library/Application Support/YourApp on macOS, ~/.config/your-app on Linux. Never in the install folder.
  • Set options.app_version to the version you release: the server uses it to block builds that are too old and to announce new ones, see Signed releases and updates.
  • cargo build --release gives one executable. ClavisAUH's CI cross-builds the Rust samples into .exe files on Linux (target x86_64-pc-windows-gnu) and runs them on Windows.
  • Run the build to OK k1 active on a clean machine before you release it.
  • Before you sell, turn on Publicly released in Integration › App settings. Every TEST key stops working, so a test key that got out cannot be used.

Questions

Does the software need a network every time it opens? Activation and heartbeats do. Without a network, the saved token keeps the software running for 7 days by default, or the length chosen on Premium, checked on the machine without the server.

Is the SDK async? No: each call blocks until the answer arrives (ureq). In async code, call it inside spawn_blocking or on a thread of its own.

How do I limit features by license plan? client.has("feature_name") tells whether the license grants that feature; create features in License plans and entitlements.

What if a customer reinstalls the system or changes machines? After a reinstall on the same machine, the server still recognizes it from its hashed hardware identifiers. A new machine takes a new seat; when none is left the customer gets device_limit, and you Reset devices on the Licenses page. See Devices, HWID and transfers.

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