ozankurt / laravel-modules-licensing
Self-hosted software licensing for Laravel: issue/validate license keys, Ed25519 signed offline keys, seat activations, private Composer gating, and a Core-free client SDK.
Package info
github.com/OzanKurt/KurtModules-Licensing
pkg:composer/ozankurt/laravel-modules-licensing
Requires
- php: ^8.3
- ext-sodium: *
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/database: ^12.0 || ^13.0
- illuminate/http: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- ozankurt/laravel-modules-core: ^1.0
- spatie/laravel-package-tools: ^1.92
- spatie/laravel-translatable: ^6.11
Requires (Dev)
- filament/filament: ^3.0 || ^4.0 || ^5.0
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.0
- pestphp/pest-plugin-laravel: ^3.0
- rector/rector: ^2.0
This package is auto-updated.
Last update: 2026-07-21 06:02:56 UTC
README
Self-hosted software licensing for Laravel. Issue and validate license keys, sign offline license files, enforce per-machine seat limits, and gate private Composer downloads — everything you need to sell a premium Laravel/Filament package, running on your own infrastructure.
Part of the KurtModules family. Headless by design, with an optional Filament admin (Filament 3, 4 & 5) and a Core-free client SDK you embed in the package you sell.
Features
- License keys — human-friendly, crypto-random keys (
ABCD-EF23-GH45-JK67). Only a keyed HMAC of the key is stored, never the plaintext. - Three policies —
perpetual,subscription(time-limited), andupdates_window(runs forever, but only downloads releases published before a cutoff — the "license expires, software keeps working" model). - Seat activations — per-machine fingerprints with a configurable limit. Re-activating the same machine is idempotent; deactivation frees the seat.
- Online + offline — a JSON API for live validation, and Ed25519-signed license files that verify entirely offline with only a public key.
- Composer gating — an HTTP-Basic bridge (email + key) that authorizes Composer
downloads against the license, front your private Satis or use the bundled
auth_requestendpoint. - Audit trail — every issue / activate / validate / composer decision is logged.
- Client SDK —
Kurt\Modules\Licensing\Client\*depends only onilluminate/*(no Core), so you can embed it in the package you sell. Includes an offline grace window so a network blip never bricks a paying customer.
Requirements
- PHP 8.4+ (with
ext-sodium, bundled in modern PHP) - Laravel 12
ozankurt/laravel-modules-core^2.0
Installation
composer require ozankurt/laravel-modules-licensing
Core is not on Packagist yet, so add it as a VCS repository in your app's
composer.json:
"repositories": [ { "type": "vcs", "url": "https://github.com/OzanKurt/KurtModules-Core" } ]
Publish config + migrations and migrate:
php artisan vendor:publish --tag=modules-licensing-config php artisan vendor:publish --tag=modules-licensing-migrations php artisan migrate
Generate a signing keypair and add it to .env (keep the signing key on the server only):
php artisan licensing:keygen # LICENSING_SIGNING_KEY=... (server only — signs license files) # LICENSING_PUBLIC_KEY=... (also embed in the package you sell — verifies them)
Server usage
Define a product
use Kurt\Modules\Licensing\Server\Models\Product; $product = Product::create([ 'slug' => 'acme-pro', 'name' => ['en' => 'Acme Pro'], 'composer_packages' => ['acme/pro'], // gated for Composer 'default_policy' => ['type' => 'subscription', 'max_activations' => 3], ]);
Issue a license
use Kurt\Modules\Licensing\Facades\Licensing; $issued = Licensing::issue($product, [ 'licensee_email' => 'buyer@example.com', 'expires_at' => now()->addYear(), ]); $issued->key; // 'ABCD-EF23-...' — show once, it is not recoverable $issued->license; // the persisted License model
…or from the CLI:
php artisan licensing:issue acme-pro buyer@example.com --seats=3 --expires="2027-01-01"
Sign a downloadable license file (offline activation)
$blob = base64_encode(json_encode(Licensing::signFileFor($issued->license))); // hand `$blob` to the customer as a .lic file
HTTP API
Mounted under the configured prefix (default licensing, throttled 60,1):
| Method | Path | Body | Response |
|---|---|---|---|
| POST | /licensing/validate |
{ key, fingerprint? } |
{ valid, reason, claims } |
| POST | /licensing/activate |
{ key, fingerprint, label? } |
{ valid, claims } / 422 / 404 |
| POST | /licensing/deactivate |
{ key, fingerprint } |
{ deactivated } |
| GET | /licensing/composer/authorize/{pkg} |
HTTP Basic (email:key) | 204 / 401 / 403 |
Client SDK (embed in the package you sell)
use Kurt\Modules\Licensing\Client\LicenseManager; // auto-resolved from config('licensing.client.*'): server_url + key $state = app(LicenseManager::class)->check(); $state->ok(); // true when valid OR within the offline grace window $state->isGrace(); // true when the server was unreachable but a recent // success is cached and still inside grace_days $state->status; // 'valid' | 'grace' | 'invalid' $state->claims; // product, policy, seats, expiry, licensee, ...
Offline verification of a signed file — no server, only the public key:
use Kurt\Modules\Licensing\Client\OfflineVerifier; $result = (new OfflineVerifier($publicKey))->verifyBlob($blob); $result->valid; // signature + expiry checked locally
The machine fingerprint is stable and opaque (no raw host data leaves the machine):
use Kurt\Modules\Licensing\Client\Fingerprint; Fingerprint::generate($installSalt);
Composer download gating
Apply the licensing.composer middleware to your private repository routes, or point
an nginx auth_request at the bundled endpoint:
location ~ ^/dist/(?<pkg>.+)\.zip$ { auth_request /auth; } location = /auth { internal; proxy_pass https://licenses.example.com/licensing/composer/authorize/$pkg; proxy_pass_request_body off; proxy_set_header Authorization $http_authorization; }
Composer sends the buyer's email as the username and the license key as the password;
the bridge authorizes against the license's product, status, and (for
updates_window) the release date.
Filament admin (optional)
If you run a Filament panel, register the version-dispatching plugin — the same call works on Filament 3, 4, and 5:
use Kurt\Modules\Licensing\Filament\LicensingPlugin; $panel->plugin(LicensingPlugin::make());
It adds a Products resource (full CRUD) and a Licenses resource (browse, edit status/policy/seats, and a one-click revoke action). Licenses are minted through the issuer/API, so the Licenses resource has no "create" form by design.
Configuration
Key .env values:
| Variable | Purpose |
|---|---|
LICENSING_SIGNING_KEY |
Ed25519 secret — signs license files (server only) |
LICENSING_PUBLIC_KEY |
Ed25519 public — verifies files (server + clients) |
LICENSING_KEY_HASH_SECRET |
HMAC secret for key hashing (defaults to app key) |
LICENSING_SERVER_URL |
Client: base URL of the licensing API |
LICENSING_KEY |
Client: this install's license key |
See config/licensing.php for the full reference.
Testing
composer test # Pest composer stan # PHPStan level 8 composer lint # Pint (dry-run)
License
MIT © Ozan Kurt