DEV Community

PermitCore
PermitCore

Posted on Originally published at permitcore.dev

How to Build a License Key System

Whether you build one yourself or use a service, every license key system is answering the same three questions: what does a key look like, how do you check one without leaking what a valid one looks like, and how do you stop a valid key from working everywhere at once. Here's the real architecture, independent of any particular vendor.

Designing the key format

A license key is a string a human might need to type, read over the phone, or paste from an email — that constrains the design more than it first seems. A sensible format groups characters for readability (PERMIT-XXXX-XXXX-XXXX-XXXX rather than one long block), and a good charset avoids characters that are easy to confuse when hand-typed or read aloud — no 0/O, no 1/I/l. A real example: ABCDEFGHJKLMNPQRSTUVWXYZ23456789 — deliberately missing the ambiguous characters, 32 symbols so each character carries 5 bits of entropy, and a 20-character key (the four XXXX groups) gives 100 bits — comfortably beyond brute-force range.

Never store the plaintext key server-side

This is the part naive implementations get wrong most often: storing the key as plaintext (or as a reversible encryption of it) means a stolen database dump hands an attacker every valid key directly. The correct approach is a one-way transform — hash the key, and look it up by hash, not by the key itself. A plain hash isn't quite enough either: a fast hash function (SHA-256 alone) lets an attacker with a stolen dump brute-force short/predictable keys offline at billions of guesses per second. The fix is a keyed hash — HMAC with a secret that lives only on your server, never in the database — so even a full database theft is useless without also stealing the separate secret.

The lookup path

// Customer sends the plaintext key in a POST body (never a URL — keeps it out of
// access logs, browser history, and CDN caches)
var hash = HMAC_SHA256(serverSecret, SHA256(plaintextKey));

// Look up by the keyed hash, never by the plaintext key itself
var license = db.LicenseKeys.FirstOrDefault(l => l.KeyHash == hash);
Enter fullscreen mode Exit fullscreen mode

This gives you the property that matters most: your server can validate a key it has never seen the plaintext form of stored anywhere, and a database breach alone — without the separate HMAC secret — is not enough to forge or verify keys.

Activation limits need a real lock, not just a count check

A naive "read the count, check it's under the limit, then increment" has a race window: two devices activating a nearly-full license within milliseconds of each other can both read the same under-limit count and both succeed, overselling a seat. The fix is an atomic conditional update (UPDATE ... SET count = count + 1 WHERE count < max, checking the database's own row affected count) or a database-level lock scoped to that one license — tested against genuinely concurrent requests, not just sequential ones, since the race only shows up under real concurrency.

What tends to get skipped, and shouldn't

A system that only handles the happy path is missing: per-key failed-lookup rate limiting (so a script can't enumerate keys by brute force), abuse detection for a valid key shared across too many distinct callers (a different problem than a rate limit catches — see how PermitCore detects this), and an offline story if your app needs to keep working without a network call (see device fingerprinting and offline validation). None of these are exotic — they're just easy to underestimate until a real customer hits the gap.

If building all of this yourself sounds like more than you want to maintain long-term, that's the actual tradeoff a licensing API like PermitCore is solving — see our honest build vs. buy breakdown for the fuller picture either way.


Originally published on the PermitCore blog. How do you handle license keys in your own apps? I'd love to hear what's worked for you.

Top comments (0)