Architecture

v1 JSON over HTTPS Bearer credentials or HMAC signatures

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_requests reaches 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_requests is 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