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 Notifications Laravel Package

tomatophp/pusher-notifications

TomatoPHP Pusher Notifications adds Pusher-powered real-time notifications to Laravel apps, enabling instant alerts, configurable broadcasting, and straightforward setup for delivering updates to users.

View on GitHub
Deep Wiki
Context7

Getting Started

Read the source before you run the install. There is no meaningful documentation — the v1.0.0 release note is a compare link. The package is small enough to audit in one sitting, and the two files that matter are the service provider (what it binds/registers) and the notification channel class (what method it reads off the notification, and what event/channel names it emits). Do this first; everything below assumes you've confirmed the names against the actual code.

1. Install and pin.

composer require tomatophp/pusher-notifications

Then open composer.json and pin the constraint to the exact version ("tomatophp/pusher-notifications": "1.0.0", not ^1.0). At v1.0.0 with a single maintainer, zero dependents, and no compatibility matrix, floating constraints are how you get a surprise breaking change on a Tuesday.

2. Configure credentials and the broadcast stack.

BROADCAST_CONNECTION=pusher
PUSHER_APP_ID=...
PUSHER_APP_KEY=...
PUSHER_APP_SECRET=...
PUSHER_APP_CLUSTER=mt1

Check whether the package ships a publishable config and what its tag is:

php artisan vendor:publish --provider="TomatoPHP\PusherNotifications\PusherNotificationsServiceProvider"

If there's no config file, the package is likely reading straight from config/broadcasting.php's pusher connection — meaning you must also have pusher/pusher-php-server installed and the connection defined. Verify with composer show | grep pusher and a quick dd(config('broadcasting.connections.pusher')).

3. Client side. You still need a WebSocket layer. That's laravel-echo + pusher-js, and it's not optional or incidental:

import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;

window.Echo = new Echo({
  broadcaster: 'pusher',
  key: import.meta.env.VITE_PUSHER_APP_KEY,
  cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
  forceTLS: true,
});

4. First use case — pick the boring one. A single-user status update ("your export is ready") on a private channel is the right first flow. It exercises the whole path end-to-end, it's easy to verify by eye, and if it silently fails nobody's pager goes off. Do not start with ops alerting or anything where quiet non-delivery is a real incident.

// app/Notifications/ExportReady.php
class ExportReady extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(public Export $export) {}

    public function via($notifiable): array
    {
        return ['database', 'pusher']; // confirm the channel key in the source
    }

    public function toArray($notifiable): array
    {
        return [
            'title' => 'Export ready',
            'export_id' => $this->export->id,
            'url' => route('exports.show', $this->export),
        ];
    }
}

Then subscribe on the client and confirm a payload actually arrives. That single round-trip is your go/no-go gate.


Implementation Patterns

Always pair the real-time channel with database. Pusher notifications are ephemeral — if the user's tab is closed, the message is gone. Persisting to database (or mail for the ones that matter) gives you replay, an unread badge, and a history view. The real-time channel is a delivery accelerator, not a store.

public function via($notifiable): array
{
    return ['database', 'pusher'];
}

Wrap it behind your own contract on day one. This is the highest-leverage thing you can do with a package at this maturity level. If 'pusher' appears in 40 notification classes, migrating to Laravel Reverb or the stock broadcast channel is a 40-file sweep. If it appears in one adapter, it's a config change.

// app/Notifications/Channels/RealTimeChannel.php
class RealTimeChannel
{
    public function send($notifiable, Notification $notification): void
    {
        if (!$notification instanceof ShouldRealTime) return;

        Notification::route('pusher', $notifiable->routeNotificationFor('pusher'))
            ->notify(/* ... */);
    }
}

Practical middle ground: define your own channel key (e.g. 'realtime') that resolves to the package's channel class via a small driver registration, so notification classes never reference the vendor name.

Shape the payload explicitly — never let Eloquent serialise itself.

public function toArray($notifiable): array
{
    return [
        'title' => $this->export->name,
        'export_id' => $this->export->id,
        'status' => $this->export->status,
    ];
}

If the channel falls back to toArray() and you return a model, you'll ship half your schema to a browser and blow through Pusher's 10KB message cap on the first chatty record. Use ->only([...]) and keep payloads under ~8KB with headroom.

Queue everything, and dispatch after commit.

class ExportReady extends Notification implements ShouldQueue
{
    use Queueable;
    public $afterCommit = true;
    public $tries = 3;
    public $backoff = [10, 60, 300];
}

Without afterCommit, a notification can fire before the transaction commits and the client will subscribe/fetch against data that doesn't exist yet — or the message arrives and points at a row that got rolled back. Without queueing, you've added a network round-trip to Pusher inside the request lifecycle of a best-effort feature.

Targeting: use private channels for anything user-specific. Public channels mean anyone with the app key can subscribe. Read the source and confirm whether the channel builds a PrivateChannel (private-notifications.{id}) or a bare Channel; if it's the latter and you're sending per-user data, override the channel name or wrap it.

Route targeting typically reads:

public function routeNotificationForPusher($notification)
{
    return 'App.Models.User.'.$this->id;
}

Confirm the exact method name the package calls — could be routeNotificationForPusher, could be routeNotificationForBroadcast. Getting this wrong means notifications go to a default channel and vanish.

Local development against Reverb/Soketi. Because these speak the Pusher protocol, you usually don't need a Pusher account to develop:

// config/broadcasting.php
'pusher' => [
    'driver' => 'pusher',
    'key' => env('PUSHER_APP_KEY'),
    'secret' => env('PUSHER_APP_SECRET'),
    'app_id' => env('PUSHER_APP_ID'),
    'options' => [
        'cluster' => env('PUSHER_APP_CLUSTER'),
        'host' => '127.0.0.1',
        'port' => 8080,
        'scheme' => 'http',
        'useTLS' => false,
    ],
],

This is also your exit-cost probe. If the package works against Reverb without code changes, your vendor lock-in is a config file. If it doesn't, you've learned something important cheaply.

Testing.

Notification::fake();

$user->notify(new ExportReady($export));

Notification::assertSentTo($user, ExportReady::class, function ($n, $channels) {
    return in_array('pusher', $channels);
});

Notification::fake() proves your code dispatched — it does not prove the transport works. For anything on a critical path, add one integration test against a local Reverb/Soketi instance in CI, or a nightly smoke test against the real Pusher app. That's the only thing that catches a bad credential or a rotated key.


Gotchas and Tips

Non-delivery is silent, and that's the whole risk. The API call succeeds, the queue job passes, the user sees nothing. Nothing in a standard Laravel health check will catch this. Concrete mitigations:

  • Implement failed() on the notification and log/alert on it. Note that a successful API call to a channel nobody is subscribed to won't reach failed() — so also log dispatch at the client boundary and compare (e.g. "sent" server-side vs "received" client-side for a sampled flow).
  • Add a Notification::failed() listener or a global NotificationFailed event handler.
  • Watch the Pusher dashboard's message and error metrics during the pilot. The Pusher debug console is the fastest way to see whether your event ever left the server.

Event-name and channel-name mismatches are the #1 cause of "it works but nothing appears." The client listener string must match exactly what the channel emits, including any package-imposed prefix.

window.Echo.private(`App.Models.User.${userId}`)
  .listen('.export.ready', (e) => { /* namespaced */ })
  .listen('ExportReady', (e) => { /* or not */ });

Laravel Echo's leading-dot convention (.EventName) is a constant source of confusion. Log the raw payload from the Pusher debug console before you start editing listeners.

Private/presence channel auth fails quietly too. Check Broadcast::routes(['middleware' => ['web', 'auth']]) in routes/channels.php, then hit /broadcasting/auth directly in the browser devtools with the socket_id and channel_name. A 403 there means the guard doesn't match the one the WebSocket handshake uses — common when you have multiple guards (admin vs user) and haven't set Broadcast::routes(['middleware' => ['auth:admin']]).

Pin pusher/pusher-php-server, not just this package. The package's constraint may be loose or undeclared. Run composer why-not pusher/pusher-php-server <version> on a branch before you commit to it. A silent major bump of the SDK is a real failure mode here.

Verify the declared PHP/Laravel ranges in composer.json yourself. The changelog discloses nothing, and "it installed on my machine" is not declared support. If it says ^10.0 and you're on 11, you are running untested code — decide deliberately whether that's acceptable for a pilot.

Message size and rate limits are real. Pusher caps messages at 10KB and bills on concurrent connections plus message volume. A "notify every follower" fan-out will produce a surprising bill and likely a rate-limit error. Batch, aggregate, or debounce before you send per-user in a loop.

Retries cause duplicate deliveries. A queued notification that retries after a timeout may deliver twice. Make client handlers idempotent — key on a notification ID and ignore repeats:

const seen = new Set();
.listen('.export.ready', (e) => {
  if (seen.has(e.id)) return;
  seen.add(e.id);
  // ...
});

Don't let the vendor type leak. No Pusher\ imports in models, controllers, services, or domain events. If you find one outside your adapter, stop and fix it — that's the exact coupling that triples your migration estimate later.

Upgrade review is a calendar item, not a habit. At zero stars and one publisher, releases won't be annotated. Assign a named engineer to diff every release against your pinned version. The diff will be small; the point is that someone is looking.

Keep the exit path warm. Because you wrapped the channel on day one, moving to Reverb (free, first-party, self-hosted) or Laravel's stock broadcast channel should be a config swap plus one adapter. Test that assumption once during the pilot by pointing the same code at a local Reverb instance. If that works, the vendor decision stays reversible — which is the only thing that makes a v1.0.0 single-maintainer dependency a reasonable bet.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager