Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Licensing Client Laravel Package

masterix21/laravel-licensing-client

Laravel client for integrating license validation into your app. Verify keys, activate or deactivate installations, and check license status against a remote licensing server, with a simple, configurable API for gating features and managing subscriptions.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish config

composer require masterix21/laravel-licensing-client:^2.0
php artisan vendor:publish --tag="licensing-client-config"

Confirm your platform requirements first: 2.0 requires a modern PHP runtime (8.4/8.5 are explicitly supported and tested, alongside Laravel 13 in CI). If you are still on PHP 8.1/8.2 or Laravel 10, stay on 1.x — check composer.json constraints before bumping.

Set the issuer before anything else

The single most important step after upgrading is configuring the issuer so it matches the value your laravel-licensing server stamps into the iss claim:

// config/licensing-client.php
'issuer' => env('LICENSING_ISSUER', 'https://licensing.example.com'),

A mismatch is not a warning — every token is rejected. Set this in .env per environment (staging and production servers usually issue different iss values) and re-run php artisan config:cache if you cache config.

Bootstrap the public key bundle

The client verifies PASETO tokens offline against an Ed25519 public key. The validator can be initialised from a previously stored bundle on boot, and the bundle is refreshed from server responses. The practical first-run flow is:

  1. Activate the license against the server (this response carries the current public key bundle, which the client applies to the validator).
  2. Persist the bundle (cache/DB/filesystem, per the package's storage driver) so subsequent requests — and queue workers — can verify tokens with no network call.
  3. Validate the token and read claims/entitlements.

First use case: gate a feature by entitlement

The most common day-to-day use is answering "is this install/tenant allowed to use feature X?" Read entitlements from the validated token rather than calling the server:

$token = Licensing::validate($storedToken);   // offline, Ed25519-verified

if ($token->entitlements()->contains('reports.export')) {
    // allow
}

Start by wiring one low-risk feature behind an entitlement check and confirming the happy path plus the "token expired" path before rolling it out broadly.

Implementation Patterns

Middleware for licensed routes

Wrap premium route groups in a middleware that validates the stored token and aborts with a 402/403 when the license is missing, expired, or lacks the entitlement. Keep the middleware thin — validation result plus entitlement check — and let the controller deal with the feature itself. This gives you a single, auditable choke point instead of scattered if (Licensing::check()) calls.

Gate/ability mapping to entitlements

Expose entitlements as Laravel gates or abilities so the rest of the app speaks idiomatic Laravel:

Gate::define('export-reports', fn ($user) =>
    $user->currentTeam->licenseToken()?->entitlements()->contains('reports.export') ?? false
);

Then use @can('export-reports') in Blade and $this->authorize(...) in controllers. The entitlements API is the natural seam between "licensing" and "authorization" — keep it there and the licensing package stays isolated to one or two files.

Polymorphic licensable models

The token now exposes licensable_type and licensable_id. Use these to bind a token to the model that owns it (a Team, Installation, Tenant, etc.) and to reject tokens being replayed against the wrong subject. A common pattern is a HasLicense trait that stores the token and compares licensable_type/licensable_id on retrieval:

abort_unless(
    $token->licensable_type === $this->getMorphClass()
    && (string) $token->licensable_id === (string) $this->getKey(),
    403
);

Scheduled re-validation (heartbeat) instead of per-request calls

Because verification is offline, you do not want an HTTP call on every request. Run a scheduled command (hourly or daily) that fetches the current token/bundle from the server, applies the new bundle to the validator, and persists both. Between runs, requests validate against the stored token locally. This is the pattern that makes the package cheap at runtime and is the whole point of the certificate-chain work in 2.0.

Graceful degradation and revocation lag

Offline verification means a revoked license keeps working until the next heartbeat. Design for that: keep the heartbeat interval short enough for your commercial tolerance, and have a license:refresh command you can trigger from an admin action ("re-sync license now"). On heartbeat failures, distinguish network errors from license errors — see the HTTP mapping notes below.

Queue workers and long-lived processes

The validator is initialised from the stored bundle on boot. Long-running queue workers (Horizon, Octane) will keep the validator they booted with. After a key rotation or bundle refresh, restart workers (php artisan queue:restart, octane:reload) or explicitly re-initialise the validator inside the job. Wrapping validation in a small service that checks a "bundle version" cache key before trusting the in-memory validator is a robust way to handle this.

Testing

Fake the client in feature tests so CI never depends on a licensing server, and keep one or two integration tests against a real/stubbed server for the token round-trip. Use Carbon::setTestNow() to exercise expiration and not-before behaviour deterministically — with a ±60s skew this matters.

Gotchas and Tips

The issuer config is the #1 upgrade trap

Tokens with an invalid iss are now rejected outright. If you upgrade without setting licensing-client.issuer, every previously working token starts failing at once. Symptom: valid-looking tokens rejected immediately after composer update. Fix the config, then php artisan config:cache — forgetting the cache refresh in production is the classic second-order failure.

Certificate chain verification can invalidate "good" tokens

Tokens carrying a certificate chain in the footer are now verified against the root public key. If your stored root key predates a server-side rotation, or your stored bundle is stale, validation fails even though the token is genuinely from your server. Recovery path: force a bundle refresh from the server (heartbeat) rather than hand-editing stored keys. Also verify that your token storage persists the bundle, not just the token — a token alone is no longer sufficient context after a rotation.

Clock skew is your friend and your debugging enemy

Expiration and nbf checks now tolerate ±60s, configurable. Two practical consequences:

  • If your app servers drift ahead of the licensing server, freshly issued tokens can be "not yet valid" and get rejected. The default 60s covers normal NTP drift; widen it only if you have a real reason, since it also extends the life of expired tokens.
  • Tests that assert exact expiry boundaries must account for the skew. A token that expired one second ago still passes in a default-configured test.

nbf validation changes activation timing

If you mint a token and immediately validate it in the same request, and the issuing server's clock is even slightly ahead, you can hit a not-before rejection. The default skew handles most of this, but be aware when writing tests or doing synchronous activate-then-validate flows.

Don't deactivate on 5xx

HTTP error mapping is improved in 2.0 (400 and 5xx are now differentiated). Treat 400-class responses as "this license/token request is wrong" (bad key, wrong issuer, malformed payload) and 5xx as "the server is having a bad day". Only the former should ever lead to marking a license invalid; a 5xx during a heartbeat must fall back to the last known good token and bundle. Getting this backwards leads to paying customers losing access during your vendor's outage.

Laravel 13 / PHP 8.5 in the test matrix ≠ requirement

CI testing Laravel 13 means compatibility, not that you must upgrade. Pin your constraint to what you actually run and let the matrix inform your upgrade planning rather than forcing it.

Server compatibility is a two-sided upgrade

Full compatibility is with laravel-licensing server v2.0.0. Upgrading the client while the server is still on 1.x (or vice versa) is the most likely source of confusing signature/issuer failures. Upgrade them together, or at least verify the issuer and bundle endpoints behave as the client expects.

Extension points worth knowing

The stored bundle on boot and the "apply bundle from response" hook are the two seams you will most likely customise: wrapping bundle persistence (e.g. encrypted at rest, or keyed per tenant) and injecting a validator factory are the standard moves when you need multi-tenant licensing or non-default storage. Keep those in a dedicated service provider so package upgrades stay painless.

Log the claims, not the token

When debugging, log issuer, not_before, licensable_type/licensable_id, and the entitlement names — never the raw PASETO or key material. These four fields resolve the overwhelming majority of "license not working" tickets: wrong issuer, clock/nbf, wrong subject binding, or missing entitlement.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core