KeyPort LogoKeyPort
Back to all guides
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-XXXX format)
  • 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
Build From Scratch Database schema + migrations Key generation + validation IP tracking + revocation ~200+ lines · 2+ weeks Use KeyPort API Single API call All features included Customer portal + webhooks ~5 lines · 1 hour

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.

Scale your product with KeyPort

Free tier available for launch and small production workloads. No credit card required.