Setup guide // Self-hosted // License API

AL License System setup guide

AL License System is the self-hosted, D1-backed license server behind the rest of the catalog. It issues keys, validates them over HTTP, binds activations to a server fingerprint, and enforces a per-key activation limit. This guide covers the D1 schema, all four endpoints, fingerprint binding, and an integration snippet.

Overview

A self-hosted license server

Every paid SKU issues a row in the licenses table, and any product, ours or yours, checks in against POST /api/license/validate. It runs on Cloudflare Pages Functions against a D1 database, so there is no server process to keep alive.

Keys carry a signed integrity check, so a forged key is rejected before any database lookup happens. Real status (active, revoked, expired) always lives in D1, the source of truth the offline check cannot fake.

Data model

D1 schema

Four tables in the license database. Price and activation limits stay server-side throughout, so a client can never set its own price or activation count.

  • products the sellable SKU catalog: sku (primary key), name, price_cents, currency, kind (paid, free or subscription), max_activations, download_key, active, created_at.
  • licenses one row per issued key: key (primary key, APLU-XXXX-XXXX-XXXX-XXXX), sku, product_name, email, status (active, revoked or expired), tier (standard, open-source or free), max_activations, stripe_session, stripe_payment, amount_cents, issued_by (webhook, free, admin or manual), expires_at, created_at, updated_at.
  • activations the ledger validate enforces against: id, key, fingerprint (hashed), label, ip, first_seen, last_seen, unique on (key, fingerprint).
  • validations an append-only audit log: id, key, sku, result (valid, invalid, revoked, over-limit or unknown), fingerprint, ip, at.

Endpoints

Validate a key

POST /api/license/validate takes a key, an optional product SKU to scope the check, and an optional fingerprint. It checks the key's signed integrity offline first, so a forged key never reaches the database, then looks it up by status and expiry.

POST /api/license/validate
{ "key": "APLU-7K2M-9QXR-4WTB-H3PZ", "product": "al-store", "fingerprint": "my-server-ip-or-cfx" }

# valid   { "valid": true, "status": "active", "activations_used": 1, "max_activations": 3 }
# over    { "valid": false, "status": "over-limit" }
# revoked { "valid": false, "status": "revoked" }

Other statuses the same call can return: unknown (key not found), wrong-product (key belongs to a different SKU), expired, and unverifiable if the datastore is unreachable.

Fingerprint binding

How activations are enforced

Send a fingerprint (a server IP, cfx code or domain) and validate binds it on first use. The fingerprint is hashed before it is stored or compared; the activations table never holds it in plain text. A repeat call from the same fingerprint just refreshes its last-seen timestamp and calling IP.

A new fingerprint counts against the license's max_activations. Once that many fingerprints are bound, the next new one gets over-limit back, with the current activations_used and max_activations in the response, until you free a slot.

Endpoints

Activate, deactivate, lookup

POST /api/license/activate is a thin wrapper over validate that requires a fingerprint in the body (400 if missing), for an explicit activation step separate from the product's own boot-time validate calls.

POST /api/license/deactivate frees one activation slot so a key can move to a new server:

POST /api/license/deactivate
{ "key": "...", "fingerprint": "old-server-fingerprint" }

# { "ok": true, "removed": 1, "activations_used": 0 }

POST /api/license/lookup is self-serve key recovery by email. It never returns keys in the response, so the API cannot be used to check whether an address has purchased; matching keys are emailed to the address on file instead, and every request gets the same generic reply.

Integrate it

Call it from anything

Validation is one HTTP POST, so any language that can make a request can check a key. Call it on every boot; the first call from a new fingerprint is what binds the activation.

# validate from any product, any language
curl -X POST https://appleluis.ltd/api/license/validate \
  -H "content-type: application/json" \
  -d '{"key":"APLU-7K2M-9QXR-4WTB-H3PZ","product":"al-store","fingerprint":"srv-01"}'

# {"valid":true,"status":"active","activations_used":1,"max_activations":3}