Tutorial• April 2026
Building a License Key System in Node.js — From Scratch vs API
What does it actually take to build license key validation yourself in Node.js? We compare it line-by-line against using an API.
JL
Jordan LeeAuthor • 5 min read
Let's be concrete. What does it actually take to build a real license key system in Node.js from scratch? And how does that compare to calling an API? I'll write both and compare them honestly.
What "From Scratch" Actually Means
A minimal but real license system needs:
- Cryptographically random key generation (
XXXX-XXXX-XXXX-XXXXformat) - Database storage (PostgreSQL, SQLite)
- Associations: key → customer, product, expiry date
- Validate endpoint returning valid/invalid with a reason
- IP tracking (people always end up needing this)
- Revocation
From Scratch: Key Generation
import crypto from 'crypto';
function generateLicenseKey(): string {
const bytes = crypto.randomBytes(16);
const hex = bytes.toString('hex').toUpperCase();
return [hex.slice(0,4), hex.slice(4,8), hex.slice(8,12), hex.slice(12,16)].join('-');
}
// → "3F7A-B2D1-9E4C-1A08"
From Scratch: Database Schema
CREATE TABLE licenses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
key TEXT NOT NULL UNIQUE,
customer_email TEXT NOT NULL,
product_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active', -- active | expired | revoked
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE license_ips (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
license_id UUID REFERENCES licenses(id),
ip_address INET NOT NULL,
last_seen TIMESTAMPTZ DEFAULT now(),
UNIQUE (license_id, ip_address)
);
From Scratch: Validate Endpoint
app.post('/validate', async (req, res) => {
const { license_key } = req.body;
const clientIp = req.ip;
const lic = (await db.query(
'SELECT * FROM licenses WHERE key = $1', [license_key]
)).rows[0];
if (!lic) return res.json({ valid: false, status: 'not_found' });
if (lic.status === 'revoked') return res.json({ valid: false, status: 'revoked' });
if (lic.expires_at && new Date(lic.expires_at) < new Date())
return res.json({ valid: false, status: 'expired' });
// IP tracking (no CIDR, no blacklist, no org-level rules)
await db.query(
`INSERT INTO license_ips (license_id, ip_address)
VALUES ($1, $2) ON CONFLICT (license_id, ip_address)
DO UPDATE SET last_seen = now()`,
[lic.id, clientIp]
);
return res.json({ valid: true, status: 'active' });
});
That's ~40 lines — but it's missing CIDR matching, org-level blacklists, platform blacklists, webhooks, customer portal, audit logs, and an admin dashboard.
The API Approach: Same Result in 5 Lines
const result = await fetch('https://api.keyport.sbs/api/v1/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.KEYPORT_API_KEY}`
},
body: JSON.stringify({ license_key })
}).then(r => r.json());
// result.valid + result.status — done
What the API Gets You For Free
- 3-tier IP blacklist (per-license, organization, platform)
- CIDR range matching
- Activation limit enforcement
- Customer portal (zero code)
- Audit logs
- Webhook events (Pro)
- Version control module (Pro)
- Custom API response payloads (Pro)
When to Build It Yourself
- Hard compliance requirement to self-host all license data
- Licensing model so exotic no existing API supports it
- You're building a licensing platform yourself
For everything else — use the API.