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#
# Rust side, in src-tauri/
cargo add tauri-plugin-keyrail
# JavaScript side, in your app root
npm install @keyrail/tauri-plugin2. Register it#
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:
{
"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#
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.
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:
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:
- Create a license in the dashboard (Licenses → New license) and copy its key.
- Run your app, paste the key: the machine shows up under Machines on the license page in the dashboard.
- Turn the network off and restart the app:
check()still returnsvalid: true(withoffline: trueonce a refresh is due). - Click Suspend on the license page and turn the network back on: the next refresh (once
refreshAfterhas passed) is refused withlicense_suspended, the plugin clears its cache andcheck()returnsvalid: 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.
Need help? See Troubleshooting or write to support@keyrail.tech.