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

Pusher Push Notifications Laravel Package

laravel-notification-channels/pusher-push-notifications

Send Laravel notifications as push notifications through Pusher's Push Notifications (Beams) service. This channel lets you deliver mobile and web push messages using Laravel's familiar notification system, with support for device interest subscriptions and rich payloads.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up (5 minutes)

composer require laravel-notification-channels/pusher-push-notifications
php artisan vendor:publish --provider="NotificationChannels\PusherPushNotifications\PusherPushNotificationsServiceProvider"

On Laravel 13, require ^4.4 — that's the release that added Laravel 13.x support. Older 4.x tags will resolve but may fail on framework internals.

If the provider name differs in your installed version, run php artisan vendor:publish with no flags and pick the package from the list. The published config/pusher-push-notifications.php is deliberately tiny — two keys, instance_id and secret_key, mapped from env:

PUSHER_BEAMS_INSTANCE_ID=00000000-0000-0000-0000-000000000000
PUSHER_BEAMS_SECRET_KEY=XXXXXXXXXXXXXXXXXXXXXXXX

Both come from the Pusher Beams dashboard (not the Pusher Channels app you may already have). The package pulls in the official pusher/pusher-push-notifications server SDK; the service provider binds it as a singleton built from that config, which is also your seam for mocking in tests.

First use case: transactional push on a status change

Add the channel to an existing notification and give the notifiable a route method:

use Illuminate\Notifications\Notification;
use NotificationChannels\PusherPushNotifications\PusherChannel;
use NotificationChannels\PusherPushNotifications\PusherMessage;

class OrderShipped extends Notification
{
    public function __construct(public Order $order) {}

    public function via($notifiable): array
    {
        return [PusherChannel::class];
    }

    public function toPushNotification($notifiable)
    {
        return PusherMessage::create()
            ->title('Order shipped')
            ->body("Order #{$this->order->id} is on its way")
            ->link("https://app.example.com/orders/{$this->order->id}")
            ->iOS()
            ->android()
            ->web();
    }
}
// User model
public function routeNotificationForPusherPushNotifications($notification)
{
    return 'user-'.$this->id;   // a Beams "interest"
}

Where to look first

  1. config/pusher-push-notifications.php — confirm it read your env values.
  2. src/PusherChannel.php — the entire delivery path is roughly 30 lines: resolve route → call toPushNotification() → publishToInterests(). Read it once and the package stops being a black box.
  3. src/PusherMessage.php — the payload builder; this is where you'll extend if you need fields it doesn't expose.
  4. The README's Usage section — short, and covers the platform-toggle methods.

The mental model that makes everything click: Beams never targets a device token. Your mobile/web client calls beamsClient.start() and subscribe("user-42"); the server publishes to the interest string. The package is a thin, well-behaved wrapper around that publish call.

Implementation Patterns

Pick an interest taxonomy before writing notifications. Three workable schemes:

  • Per-user: user-{id} — every device of one user gets the push; simplest, best default.
  • Segment: plan-pro, locale-nl, beta-testers — broadcast to cohorts; devices subscribe to several.
  • Hybrid: publish to ['user-42', 'plan-pro'] in one call for user-specific plus fallback.

Store the interest on the notifiable (push_interest column) or derive it deterministically, so the client and server can't drift.

Use on-demand notifications for broadcasts — no model required, because the channel resolves routes through routeNotificationFor('PusherPushNotifications'):

Notification::route('PusherPushNotifications', ['segment:beta-testers'])
    ->notify(new BetaAnnouncement($copy));

This is the pattern for admin-triggered campaigns, release notes, and "server is down" ops alerts, and it keeps broadcast logic out of your Eloquent models.

Queue them, but commit first. A push that fires before the DB transaction commits is a support ticket waiting to happen:

class OrderShipped extends Notification implements ShouldQueue
{
    public $afterCommit = true;
}

Queueing also keeps a slow Beams API call out of the request cycle — the SDK is a blocking HTTP call.

Respect per-user platform preferences. Because ->iOS(), ->android(), ->web() scope the payload sections, you can drive them from a stored preference:

$message = PusherMessage::create()->title($title)->body($body);

foreach ($notifiable->push_platforms ?? ['ios', 'android', 'web'] as $platform) {
    match ($platform) {
        'ios'     => $message->iOS(),
        'android' => $message->android(),
        'web'     => $message->web(),
    };
}

Without any of these calls, the message goes out broadly — be explicit in production code.

Centralise payload shape. Push copy drifts fast. A small base class (abstract class PushNotification extends Notification) with a final toPushNotification() that delegates to title()/body()/link() on the subclass keeps truncation, deep-link format, and icon URLs consistent across dozens of notifications.

Chunk fan-out. Publishing to many interests in one call hits API limits, and each publish is a network round trip. Loop in batches of a handful and wrap in retry():

collect($interests)->chunk(10)->each(
    fn ($chunk) => Notification::route('PusherPushNotifications', $chunk->all())
        ->notify(new Announcement($copy))
);

Test by mocking the SDK, not HTTP. Most versions of the underlying SDK use Guzzle directly, so Http::fake() will not intercept it. Bind a mock of the bound SDK class instead and assert on the payload:

$this->mock(\Pusher\PushNotifications\PushNotifications::class, function ($mock) {
    $mock->shouldReceive('publishToInterests')
        ->once()
        ->with(['user-1'], \Mockery::on(fn ($payload) =>
            str_contains(json_encode($payload), 'Order shipped')
        ));
});

(new OrderShipped($order))->toPushNotification($user);

That single test covers the part you actually control; the rendering on the device is a manual-QA concern.

Bypass the channel when you outgrow it. For data-only pushes, collapse keys, TTLs, APNs mutable-content, or publishToUsers(), write a 40-line custom channel that injects the same SDK singleton and calls it directly. You lose nothing — the package's value is the notification-system glue, not the payload builder.

Gotchas and Tips

Pusher Channels ≠ Pusher Beams. Two different products, two different credential sets, two different SDKs. If you already use pusher/pusher-php-server for websockets, those env vars (PUSHER_APP_ID/PUSHER_APP_KEY) are not the ones this package needs. Mixing them yields authentication errors that read like "instance not found."

toPushNotification() vs toPushNotifications(). The convention in this family of channels is singular, but forks and version bumps have flipped it. If you get Call to undefined method ...::toPushNotification(), open vendor/laravel-notification-channels/pusher-push-notifications/src/PusherChannel.php and read the method it calls — 10 seconds, versus 30 minutes of guessing.

A null route is a silent no-op. If routeNotificationForPusherPushNotifications() returns null, '', or an empty array, the channel returns early and nothing happens — no exception, no log line. This is the #1 "push notifications aren't working" cause. Add an assertion or a Log::debug() in the route method during rollout.

Publishing to an empty interest succeeds. Beams returns a successful response with zero recipients. No error, no warning. Before blaming the server, verify on-device that subscribe('user-42') actually ran — interest strings are case-sensitive and must match byte-for-byte between client and server.

Delivery is at-most-once from your side, but queue retries can double-send. The SDK throws on non-2xx, so a failed notification bubbles up and your queue retries it. Beams has no idempotency key, so a timeout-then-retry can deliver twice. For user-visible pushes, consider $tries = 1 plus your own alerting rather than silent retries.

Config caching and queue workers. php artisan config:cache bakes Beams credentials into the cached config. Deploy a new secret key and the workers happily use the old one until restarted. php artisan queue:restart should be in every deploy script that touches these env vars.

Payload size and truncation. APNs caps payloads around 4 KB. Long body() values get truncated at the OS level (and iOS truncates differently from Android). Write push copy to ~120 characters and treat title/body as the whole message — link() is not rendered by the OS, it's data your app must handle in its notification delegate or service-worker notificationclick.

Silent vs visible pushes. The message builder produces alert-style payloads. Data-only/silent pushes (background sync) aren't a first-class concept here; if you need them, extend PusherMessage or drop to the SDK.

Local development is awkward. There's no Beams sandbox or fake. You need a real instance, a real device, and for iOS a real APNs provisioning profile — simulators have spotty push support. Keep a dedicated dev instance ID in .env.testing-adjacent config and never let staging and production share interests like all-users.

Budget for stewardship. The package is small (a few hundred lines) and maintenance is mostly framework-compat bumps: 4.4.0 was purely Laravel 13.x support, nothing in the channel or message API changed. Pin ^4.4 in composer.json if you're on Laravel 13, and read the README's support matrix before a framework upgrade — releases land roughly once a Laravel version, so plan the package bump as part of the framework bump, not after it. If it ever lags, forking is cheap: PusherChannel and PusherMessage together are less than an afternoon's work to own outright.

Extension points, in order of preference: (1) subclass PusherMessage and override toArray(); (2) write a custom channel injecting the SDK singleton; (3) rebind the SDK class in a service provider to add logging, metrics, or a wrapper. Never edit vendor/.

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.
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names