All guides

How to Build a License Key System

The real architecture behind a license key system — key format design, why servers should never store plaintext keys, and the lookup mechanism that makes validation fast and safe.

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

For complete C#, Python and Node.js generation examples, see how to generate license keys; this article focuses on the surrounding system.

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

The lookup can validate a presented key without decrypting browser-created ciphertext. Checking guesses against keyed HMAC values needs the separate HMAC secret. This does not protect weak passphrases from guessing against authenticated ciphertext, and server-managed Store delivery has a different recovery boundary. Review the workflow/access reference.

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.

Put the guide into practice

Build your first licensed product without building a license server.

Create a free PermitCore account, issue a test license, and validate it from your application.