@velkin/license
v0.1.1
Published
Runtime license validation for Velkin Pro
Readme
@velkin/license
Runtime license validation for Velkin Pro. Invalid or missing keys show watermarks and console warnings only; components are not hard-blocked.
import { LicenseInfo } from "@velkin/license";
// Vite example. Use NEXT_PUBLIC_VELKIN_LICENSE_KEY in a Next.js client module.
LicenseInfo.setLicenseKey(import.meta.env.VITE_VELKIN_LICENSE_KEY ?? "");The key must reach browser code and is therefore not a server secret. Keep it out of public source control and use a domain-scoped license where possible.
Plans
| Plan | Key required | Seats |
|------|--------------|-------|
| Free (MIT @velkin/ui) | No | — |
| Pro | Yes | 1 |
| Team | Yes | 5 |
| Enterprise | Yes (contract) | Unlimited |
Key format
VLK2.<base64url(payload)>.<base64url(ed25519-signature)>
Payload: { v: 2, plan, seats, exp, org?, trial? } where exp is cover-until (subscription end).
New keys also contain kid, jti, and iat. Verification uses a public-key ring so signing keys
can rotate without immediately breaking previously issued keys. Missing kid is rejected.
Clients also fetch live public keys from Official (GET /api/v1/license/keys) so Velkin can
rotate the signing private key on the server without republishing this package. Baked keys
remain as offline fallback; remote keys merge into the runtime ring (cached in localStorage).
// Default keysEndpoint is https://velkinui.com/api/v1/license/keys
LicenseInfo.configure({
syncEndpoint: "https://velkinui.com/api/v1/license/check",
keysEndpoint: "https://velkinui.com/api/v1/license/keys",
});Version lock
When a Pro build release timestamp is registered (baked at publish via pro-release-at.generated.ts):
- Build released on or before
exp→ remains valid for that build - Build released after
exp→ statuspackage_too_new(watermark) until renewal - Trial keys always calendar-expire
- Renewal mints a new key with a later
exp— replaceVELKIN_LICENSE_KEY/NEXT_PUBLIC_VELKIN_LICENSE_KEY. The previous key still unlocks builds released before itsexp.
Revocation and domains (soft DRM)
Validation is offline-first via the embedded public key. After a valid key with a jti is loaded,
the package makes a best-effort request to https://velkinui.com/api/v1/license/check (or your
configured endpoint). It sends only the non-secret license identifier.
- Same session: if the endpoint reports
revoked, status flips torevokedimmediately and Pro watermarks re-apply viaLicenseInfo.subscribe(no reload required). - Recheck: throttled to about once per hour per tab; also rechecks when the document becomes visible again (long-lived SPAs).
- Fail open: network, storage, timeout, and CSP failures never block rendering and leave a
cryptographically valid offline key as
valid. - Domains: optional signed
domainsuse exact hostnames or*.example.compatterns. In the browser the current hostname is checked; during SSR,NEXT_PUBLIC_SITE_URL/VERCEL_URLare used when present.
// Optional: self-host the status endpoint or disable background checks.
LicenseInfo.configure({ syncEndpoint: "https://licenses.example.com/v1/check" });
LicenseInfo.configure({ syncEndpoint: "" });
// Optional: force-refresh public keys after a server-side rotation.
await LicenseInfo.refreshPublicKeys();
// Optional: react when status changes (guards do this automatically).
const stop = LicenseInfo.subscribe((status) => {
console.log("license status", status);
});
stop();Seats in the payload are contract metadata, not a runtime counter. See https://velkinui.com/license.
