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);
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.