Licensing a Desktop App
A quick look at offline-first licensing for desktop apps.
Building a desktop app is hard enough. Adding licensing shouldn't be.
Most licensing solutions were built for the SaaS era—they expect your app to phone home on every launch. But desktop apps need to work offline. In airplane mode. Behind corporate firewalls. In basements with spotty WiFi.
The Problem with Traditional Licensing
Traditional license key systems have three major pain points:
- Key management: Users lose keys. They email you. You dig through records. Repeat.
- Validation servers: Your app depends on your server's uptime. If you're down, your users can't work.
- Offline support: Most solutions either don't work offline or require complex caching logic.
The Offline-First Approach
Paycheck takes a different approach. Instead of license keys, we use signed JWTs—cryptographic receipts that your app can verify locally using just a public key.
Here's how it works:
- User buys your app (Stripe or LemonSqueezy)
- They get an activation code via email
- Your app exchanges the code for a signed JWT
- From then on, validation is local—no network required
The JWT contains everything your app needs:
- License tier (free, pro, enterprise)
- Feature flags
- Expiration dates
- Device information
Implementation in Tauri
use paycheck_sdk::{Paycheck, PaycheckOptions};
// Embed your public key at build time
const PUBLIC_KEY: &str = "MCowBQYDK2VwAyEA...";
fn check_license() -> Result<bool, Error> {
let paycheck = Paycheck::new(PUBLIC_KEY, PaycheckOptions::default())?;
let result = paycheck.validate(None);
if result.valid {
if let Some(claims) = result.claims {
return Ok(claims.tier == "pro");
}
}
Ok(false)
}That's it. No HTTP calls. No API keys to protect. No server to maintain.
Handling Activation
For desktop apps, users typically enter an activation code. This is the one network call your app needs to make:
async fn activate(code: &str, device_id: &str) -> Result<String, Error> {
let jwt = paycheck.redeem(code, device_id, "machine").await?;
save_to_disk(&jwt)?;
Ok(jwt)
}After activation, the JWT is stored locally. Your app validates it on every launch without touching the network.
Recovery Without Support Tickets
What happens when a user gets a new machine? They don't need their old license key—they just request a new activation code using their purchase email. Self-service, no intervention needed.