minishlink/web-push
WebPush is a PHP library implementing the Web Push protocol (RFC 8030) for sending push notifications to browser endpoints. It handles Subscription objects, payload encryption, VAPID authentication, and delivery to service workers. Requires PHP 8.2+ with curl, openssl, and mbstring.
Architecture fit. minishlink/web-push is a single-purpose protocol library, not a framework component: it encrypts payloads per RFC 8291/8030, signs VAPID JWTs per RFC 8292, and dispatches HTTP requests to browser vendor push services (FCM, Mozilla autopush, WNS, Apple). It has no Laravel service provider, no config publish, no facades, and no persistence layer. Architecturally that is a feature for us — it stays at the edge of the domain, and everything opinionated (subscription storage, retries, channel routing, telemetry) is ours to own. Fit is strong if we treat it as a transport primitive behind a thin Laravel wrapper; it is a poor fit if stakeholders expect a batteries-included push product.
Integration feasibility. High. Composer install, PHP 8.2+, and standard extensions (curl, openssl with EC, mbstring, gmp/bcmath recommended for crypto performance). Guzzle is already in our transitive dependency graph via Laravel's HTTP client, and the library supports injecting a Guzzle client, so proxy, TLS, and timeout policy can be centralized. The API surface is small and stable: Subscription::create() from the browser's PushSubscription.toJSON() output, queueNotification() + flush() for batches, sendOneNotification() for single sends, and MessageSentReport objects for outcomes including isSubscriptionExpired(). A competent mid-level engineer can produce a working spike in under a day.
Technical risk.
.env on a single node.flush() blocks on Guzzle. Calling it in a request cycle is a latent latency incident; queue-only is mandatory.flush() returns a \Generator that can be iterated exactly once. Naive retry or "log then summarize" code silently loses results.urgency, and the contentEncoding transition from aesgcm to aes128gcm, are reminders that browser vendors — not the library — set the compatibility bar.Key questions.
pushManager.subscribe() flow, and is that work funded and scheduled?Stack fit. Web push complements, and does not replace, APNs/FCM mobile push: it targets browsers and installed PWAs. It slots naturally into Laravel's notification ecosystem as a custom channel (vonage, mail, database are the familiar analogues), backed by a queued job per batch. Recommended shape:
config/webpush.php — VAPID subject, key references resolved from secrets, default TTL/urgency/topic/batch size, timeouts.WebPush instance as a singleton (or a small factory keyed by tenant, if multi-tenant).push_subscriptions table: user_id, endpoint (unique, hashed index), public_key, auth_token, content_encoding, user_agent, last_success_at, last_failure_reason, disabled_at.WebPushChannel + WebPushMessage value object so product code says $user->notify(new InvoiceReady($invoice)).SendWebPushBatch job that flushes in bounded batches and persists MessageSentReport outcomes.Migration path. Greenfield for most teams, so "migration" is really three parallel tracks: (a) backend library adoption, (b) subscription capture endpoints and frontend service worker, (c) key provisioning and secret distribution. If replacing a vendor, run both in shadow for one release cycle — capture subscriptions in both formats where possible, compare delivery rates, then cut over. Note that subscriptions are vendor-agnostic (endpoint + keys), so export/import from a vendor is feasible but VAPID key continuity is not: subscribers must re-subscribe if we introduce our own key pair, which argues for launching with our key from day one.
Compatibility. Enforce PHP 8.2+ in composer.json platform config; require ext-curl, ext-openssl, ext-mbstring, and prefer ext-gmp/ext-bcmath for encryption throughput. Store contentEncoding per subscription rather than assuming a global default, since browsers differ and older subscriptions may carry aesgcm. Pin a major version and treat its PHP matrix as an input to platform upgrade planning. Validate against the four major push services explicitly; a green CI badge is not evidence of browser-vendor behavior.
Sequencing.
composer.json platform constraints.WebPushChannel, outcome persistence, expired-subscription pruning.Rough envelope: 3–5 engineer-weeks backend, plus separate frontend effort.
Maintenance. Low code-maintenance, moderate operational-maintenance. The library is MIT, actively released, and its surface area is small enough that upgrade diffs are readable. Real cost sits in the edges: PHP upgrade cadence (version matrix), subscription table hygiene, and browser vendor behavior changes. Budget a recurring slice — roughly a day per quarter for dependency and vendor-compat review — plus a defined owner for the VAPID key.
Support. No commercial support; community issues and docs only. The README is genuinely good and covers TTL, urgency, topic, batching, and payload sizing — the questions teams actually get wrong. Escalation path for a production incident is "read the code," which is viable here because the codebase is small. Set that expectation explicitly with stakeholders before go-live; this is not a vendor with an SLA.
Scaling. Throughput is bounded by two things: encryption CPU and push-service rate limits. Mitigations are all configuration, not code — batchSize (default 1000, lowered to protect memory), setReuseVAPIDHeaders(true) to avoid re-signing JWTs across a flush, and horizontal queue-worker concurrency. Sending is stateless per notification, so scaling is embarrassingly parallel; the hard constraint is per-vendor throttle behavior (429s), which needs backoff, not brute force. At hundreds of thousands of subscribers, the pruning job and the endpoint index matter more than the send path.
Failure modes to instrument.
isSubscriptionExpired() → soft-delete the row; never retry. Stale subscriptions are the dominant long-run data quality issue.Generator misuse → silently missing telemetry. Mitigate by materializing reports inside the job and persisting before any other logic.urgency → reported Safari failures. Set a default explicitly.subject → silent rejection by vendors. Validate at boot, fail the health check.memory_limit alerting.Ramp-up. Developer ramp-up is short — a half-day to first successful send using the README example and the linked sample project; API shape (queue → flush → reports) maps cleanly onto concepts Laravel engineers already hold. Operational ramp-up is the longer pole: teams need to internalize that (a) VAPID keys are effectively immutable, (b) all sends belong on the queue, (c) subscription expiry is normal and must be handled, and (d) delivery is best-effort with no read receipt. One internal runbook covering key custody, pruning cadence, throttle response, and browser-compat regression testing will do more for reliability than any additional code.
How can I help you explore Laravel packages today?