License key delivery
Besides subscription plans, the starter can sell a one-time product of your own and deliver a license key automatically.
Lemon Squeezy checkout ──order_created──▶ /api/webhooks/lemonsqueezy
│ variant ∈ LEMONSQUEEZY_VARIANT_LICENSE
▼
issueLicense() → licenses table (key AW-XXXX-XXXX-XXXX-XXXX)
▼
deliverLicense()
├─ GitHub: invite custom field `github` to LICENSE_GITHUB_REPO (read-only)
└─ Email: key + signed link /api/download?token=KEY.SIG
└─▶ 302 LICENSE_DOWNLOAD_URL
/admin/licenses ← list, search, resend, revoke, issue by hand
order_refunded → license revoked, download link stops working
Setup
- Lemon Squeezy: create the product (single payment). Copy its variant id
into
LEMONSQUEEZY_VARIANT_LICENSE(comma-separate several). Under Checkout → Custom fields add a text field with keygithublabelled "GitHub username (for repo access)". Point the webhook athttps://your-app/api/webhooks/lemonsqueezywithorder_createdandorder_refunded. - Email: set
RESEND_API_KEYandEMAIL_FROM(a verified Resend domain). Without a key, emails are printed to the server log and the admin page showslogged. - GitHub invite (optional):
LICENSE_GITHUB_REPO=owner/private-repoandLICENSE_GITHUB_TOKEN— a fine-grained token with Administration: write on that repo (needed to add collaborators). Buyers are added withpullpermission. - Download (optional):
LICENSE_DOWNLOAD_URL— a release zip, R2/S3 object or GitHub release asset. Links are signed withLICENSE_TOKEN_SECRET(defaults toAUTH_SECRET). - Admin:
ADMIN_EMAILS=you@company.com, sign in, open/admin/licenses. APP_URL=https://your-appso emails carry absolute links.
Behaviour
- One license per Lemon Squeezy order: a replayed webhook returns the existing key.
- Unpaid orders (
status != paid) are ignored;order_refundedsetsstatus = revoked. /api/downloadrejects bad signatures (400), unknown keys (404) and revoked keys (403), and counts successful downloads.resendfrom the admin page repeats the whole delivery (invite + email) and records the outcome.- Keys use the alphabet
A–ZwithoutO/Iand digits2–9, so they can be read aloud.
Extending
- Validate keys in your own product:
getLicenseByKey(key)?.status === "active". - Per-seat licenses: add a
seatscolumn and count activations in a new table. - Lemon Squeezy also has built-in license keys; this flow keeps the keys in your database so you can revoke, re-send and gate downloads without another API call.