Get started

Quickstart for Tauri

Add license keys and offline activation to a Tauri 2 app with tauri-plugin-keyrail.

This guide adds licensing to an existing Tauri 2 app. The Rust side (the keyrail crate, wrapped by the plugin) caches and verifies the certificate; your frontend calls four commands: check, activate, deactivate and status. Network and disk work runs on Tauri’s blocking thread pool, never on the UI thread.

Before you start#

In the dashboard, create a product (Products → New product) and at least one plan (Plans → New plan); the Get started checklist on the Overview screen walks you through both. Copy two values from the product page, under Signing keys:

  • the Product ID, like prd_7k2m9q4x1c8d3f6h0j5n;
  • the Active public key k1, a 43-character base64url string.

Both are public: they are meant to be compiled into your app. To create test licenses without a payment provider, use Licenses → New license in the dashboard or POST /v1/licenses.

1. Install the plugin#

bash
# Rust side, in src-tauri/
cargo add tauri-plugin-keyrail

# JavaScript side, in your app root
npm install @keyrail/tauri-plugin

2. Register it#

src-tauri/src/lib.rs
use tauri_plugin_keyrail::Config;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let keyrail = Config::new(
        "prd_7k2m9q4x1c8d3f6h0j5n",
        [("k1", "pZ3vC0x6hYjQ8m1K4tR9wN2bF7sL5dA0eUqTgHiOcVk")],
    );
    // Debug builds only: point the plugin at a local service (see "Try it locally" below).
    #[cfg(debug_assertions)]
    let keyrail = match std::env::var("KEYRAIL_API") {
        Ok(api_base) => keyrail.api_base(api_base),
        Err(_) => keyrail,
    };

    tauri::Builder::default()
        .plugin(tauri_plugin_keyrail::init(keyrail))
        .run(tauri::generate_context!())
        .expect("error while running the app");
}

The app version checked against maxMajor defaults to the version of your Tauri app, and the certificate is cached in keyrail-license.json in the app data directory. Optional settings on Config: .api_base(…), .app_version("1.4.2"), .cache_path("license.json") (relative paths are resolved against the app data directory) and .machine_name(None) to send no machine name (the host name is sent by default). An invalid configuration, such as a malformed public key, makes the app fail at setup.

Allow the frontend to call it in your capability file:

src-tauri/capabilities/default.json
{
  "identifier": "default",
  "windows": ["main"],
  "permissions": ["core:default", "keyrail:default"]
}

keyrail:default allows the four commands. For finer control, use keyrail:allow-activate, keyrail:allow-check, keyrail:allow-deactivate and keyrail:allow-status.

3. Check at startup, activate on demand#

src/license.ts
import { activate, check, deactivate, isKeyrailError, type LicenseState } from '@keyrail/tauri-plugin';

// At every launch: verifies the cached certificate offline. Once a day (after refreshAfter),
// it revalidates online first; if the network is down, the cached certificate is used.
// License problems are reported in the state; check() rejects only for environment errors.
export function currentLicense(): Promise<LicenseState> {
  return check();
}

// From your "Enter license key" dialog.
export async function activateKey(key: string): Promise<{ ok: boolean; message?: string }> {
  try {
    const certificate = await activate(key); // the verified certificate
    return { ok: true, message: `Licensed to ${certificate.licensee ?? 'you'}` };
  } catch (e) {
    if (isKeyrailError(e) && e.code === 'activation_limit') {
      return { ok: false, message: 'This key is already used on the maximum number of machines.' };
    }
    if (isKeyrailError(e)) return { ok: false, message: e.message };
    throw e;
  }
}

// From a "Deactivate this computer" menu item: frees the seat, then clears the cache.
export const release = () => deactivate();

check() resolves to a LicenseState: { valid, reason, certificate, offline, maskedKey }. Only valid: true unlocks the app. reason is null when valid, otherwise no_license (never activated on this machine), one of the verification outcomes (expired, wrong_machine, version_not_covered, clock_tampered…) or a definitive server refusal (license_expired, license_suspended, license_refunded, license_revoked, unknown_key, unknown_product, not_activated). certificate holds the last authentic payload (plan, entitlements, status, licensee, validUntil, expiresAt…), even when it no longer grants access. offline: true means the online refresh failed and the cached certificate was used. maskedKey is the key with its middle groups hidden, for a settings screen.

activate and deactivate reject with a KeyrailError ({ code, message }, use isKeyrailError): a client API error code (unknown_key, license_expired, activation_limit, rate_limited…), invalid_request for a badly formatted key, network_error or invalid_response when no usable answer came back, invalid_certificate when the certificate returned does not verify with the configured public keys (bad signature, unknown kid; nothing is cached), or a verification outcome of the new certificate. When deactivate rejects with network_error, the cache is kept: retry later. status() returns the offline-only state, with no network call.

When an online refresh is refused definitively (403 license_*, 404 unknown_key or not_activated), the plugin clears its cache and check() reports that code; network failures, rate_limited and server errors keep the cached certificate while it is valid.

4. Gate features#

Entitlements are strings you define on the plan. They are inside the signed certificate, so they cannot be edited on the customer’s machine.

ts
import { hasEntitlement } from '@keyrail/tauri-plugin';

const canExport = hasEntitlement(await currentLicense(), 'export');

The webview can be modified: check from Rust too, before running a privileged command. License::check() is blocking (it may refresh online), so run it off the async runtime:

rust
use tauri_plugin_keyrail::KeyrailExt;

#[tauri::command]
async fn export_pdf(app: tauri::AppHandle) -> Result<(), String> {
    let keyrail = app.keyrail().clone();
    let allowed = tauri::async_runtime::spawn_blocking(move || {
        keyrail
            .license()
            .check()
            .is_ok_and(|certificate| certificate.has_entitlement("export"))
    })
    .await
    .map_err(|e| e.to_string())?;
    if !allowed {
        return Err("Export needs a license".into());
    }
    // ...
    Ok(())
}

5. Try it locally#

From a checkout of the Keyrail repository, cd e2e && npm run demo-server starts a throwaway local service (wrangler dev, local database) on 127.0.0.1:8787, creates a product, a perpetual plan and a test license, and prints KEYRAIL_API, KEYRAIL_PRODUCT, KEYRAIL_PUBLIC_KEY (kid:key) and the test license key. Put the printed product ID and public key in Config::new, and run tauri dev with KEYRAIL_API=http://127.0.0.1:8787/v1 (read by the snippet above in debug builds). examples/tauri-demo in the repository reads all three variables. Stop the server with Ctrl+C: its database is deleted.

Then, against the local service or production:

  1. Create a license in the dashboard (Licenses → New license) and copy its key.
  2. Run your app, paste the key: the machine shows up under Machines on the license page in the dashboard.
  3. Turn the network off and restart the app: check() still returns valid: true (with offline: true once a refresh is due).
  4. Click Suspend on the license page and turn the network back on: the next refresh (once refreshAfter has passed) is refused with license_suspended, the plugin clears its cache and check() returns valid: false. An app that stays offline keeps access until its current certificate expires. See how changes reach installed apps.

Next: connect Paddle Billing or Lemon Squeezy so purchases create licenses automatically.

View as Markdown

Need help? See Troubleshooting or write to support@keyrail.tech.