Protecting Electron Apps from Piracy with Keymint
Clifford3 min read
indie developerelectronguidelicense keyskeymint
This is an illustrative example, not a customer case study. The app, the timeline and the numbers below are hypothetical and show what a typical integration looks like.
After 8 months of building a screen recording tool for macOS, I had 200 beta users and was ready to launch. The problem: I had no way to charge for it. No license keys, no activation flow, no way to prevent someone from copying the .dmg and sharing it.
I looked at building my own licensing server. A week of research later, I'd need: a database for keys, an activation API, hardware fingerprinting, a management dashboard, a customer portal, and Stripe integration for subscriptions. I'm a solo developer. That's a full-time project on top of my actual product.
What I Built Instead
I used Keymint for the licensing layer and kept my focus on the recording tool. The setup took about 4 hours total: created a product in the dashboard, installed the SDK with npm install keymint, added activation logic to my Electron main process, and created a Stripe webhook handler that issues keys on payment.
The Activation Flow
My Electron app checks for a stored license key on launch. If found, it calls client.activateKey() against Keymint's API with a stable installation ID for the machine. If the key is valid and the activation count hasn't been exceeded, the call succeeds and the app opens. The SDK throws on any rejected or failed request, so the app tells a rejected license (HTTP 401, 402 or 403) apart from a temporary failure. If the user is offline, I cache the last-known-valid state and re-check periodically.
typescript
const client = new KeyMint(process.env.KEYMINT_CLIENT_API_KEY!);
try {
await client.activateKey({
productId: 'YOUR_PRODUCT_ID',
licenseKey,
hostId: KeyMint.getOrCreateInstallationId(),
});
return 'valid';
} catch (error) {
// 401, 402 or 403: the license was rejected. Anything else (offline,
// 429, 5xx) is temporary, so keep the cached state.
const status = (error as { status?: number }).status;
return status === 401 || status === 402 || status === 403 ? 'rejected' : 'unknown';
}What I Didn't Have to Build
- Hardware fingerprinting —
KeyMint.getOrCreateInstallationId()for thehostId, Keymint handles binding server-side - Activation counting — set
maxActivations: 3and the API enforces it - License revocation — hit
admin.blockKey()and the license stops working on the next check - Customer portal — on the Standard plan, Keymint provides OTP-based login where customers can reset activations
- Stripe webhook plumbing — I just added my own handler to create licenses on
invoice.paid
Illustrative Numbers
| Metric | Building it myself | With Keymint |
|---|---|---|
| Time to launch licensing | 3-4 weeks (estimated) | About 4 hours (estimated) |
These are example estimates for an integration like this one, not measured results.
What I'd Do Differently
- Use a license template from the start instead of generating 50 keys manually
- Use outbound webhooks earlier — I now pipe
device.activatedevents to Discord to spot unusual activation patterns - Add a trial mode — time-limited trial keys with
expiryDateset to 14 days before requiring purchase, per our trial design guide
Advice for Other Indie Devs
Don't build a licensing server. It's a solved problem. Platforms like Keymint handle key generation, activation tracking, machine binding, floating seats, offline verification, and customer portals. You wire up an SDK, set a few parameters, and ship. In this example, the 4 hours spent integrating Keymint save the 3-4 weeks a custom server would take, time better spent on the stuff customers actually pay for.