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 optional key recovery by email. Configure MAIL_SEND_URL and MAIL_SEND_TOKEN before exposing it. Without them the endpoint fails before account lookup and directs the buyer to support; it never claims an email was sent when no sender exists.

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}