Architecture
Layers
HTTP / CLI
│
▼
Controllers · Form Requests · Policies · API Resources
│
▼
Actions · Services (Licensing · Distribution · Security)
│
▼
Models · Enums · Support
Business rules live in app/Services/Licensing. Controllers translate HTTP into DTOs, call a service, and render the result. Nothing in resources/views decides anything.
The licensing core
App\Services\Licensing\LicenseService implements App\Contracts\Licensing\LicenseManager:
$license = License::issue(IssueLicenseData::for($product, $customer)
->expiresAt('2027-12-31')
->maxDomains(1));
$license = $licenses->issue(new IssueLicenseData(
product: $product,
customer: $customer,
expiresAt: Carbon::parse('2027-12-31'),
));
It is the only place that decides whether an installation may run. The HTTP API and any future product integration are thin adapters over it, so the rules cannot drift between entry points.
Supporting services
| Service | Responsibility |
|---|---|
LicenseKeyGenerator |
CSPRNG key generation, uniqueness retry |
DomainNormalizer |
Canonicalise and classify any host form |
DomainGuard |
Single authority on domain permission |
LicenseCertificateSigner |
Issue and verify Ed25519 certificates |
SigningKeyManager |
Provision, rotate and publish signing keys |
ApiRequestVerifier |
Bearer/HMAC authentication, replay protection |
ApiRateLimiter |
Composite rate limiting |
PackageValidator |
ZIP inspection before storage |
ReleaseService |
Version and package authoring |
DownloadService |
Temporary signed URLs |
UpdateService |
Update eligibility |
SecurityAlertService |
Heuristic alerts |
AuditLogger |
The immutable trail |
DashboardService |
Read-only aggregation |
Adding a product
Creating a product requires no change to the licensing core. Everything the core reads — grace period, activation limits, domain policy, release gating — lives on the product row and is already read generically.
Data model
products ──┬── product_versions ──── release_packages
├── api_keys
├── webhook_endpoints
│
customers ─┴── licenses ──┬── allowed_domains
├── installations ──── license_activations
├── license_events
└── downloads
Cross-cutting: api_requests · audit_logs · security_events · settings · signing_keys
Identifiers:
- ULID for products, customers, licenses, installations, versions — sortable by creation time, which keeps inserts append-only on the InnoDB primary key even when
api_requestsreaches hundreds of millions of rows. - Auto-increment for users, roles, permissions.
- Char(64) hash for license keys and API secrets, so lookups never touch ciphertext.
Storage of secrets
| Secret | Storage |
|---|---|
| License key | encrypted cast + SHA-256 for lookups + peppered fingerprint |
| API secret | SHA-256 for Bearer compare + encrypted copy for HMAC verify |
| Signing private key | encrypted cast only; never serialised |
| Webhook secret | encrypted cast, shown once |
| SMTP password | encrypted at rest |
LICENSIFY_KEY_PEPPER |
Environment only — never in the database |
APP_KEY and LICENSIFY_KEY_PEPPER must be backed up separately. Losing them means every encrypted column is unreadable and every license fingerprint must be recomputed.
Performance
- License lookups hit a unique index on
key_hash— never a table scan. api_requestsis written by a queued job so validation never waits on analytics writes.- Rate-limit and nonce state lives in the cache (Redis in production).
- Product metadata and settings are cached with explicit invalidation.
- Signing keys and the public keyring are cached and flushed on rotation.
- Offline capability means the common path is not hitting the network at all.
The cached-status path still honours revocation: every state change bumps the license's offline_revocation_counter and clears its cache entry.
Extending to multiple vendors
The schema already isolates products, and every model that matters carries a product_id. Before offering this to third parties you would add an organizations table and a nullable organization_id to products, API keys and webhook endpoints. The licensing core itself needs no change — it resolves policy through the product, never through a hardcoded vendor.
Page 8 of 8