Notifications · Open source
Laravel Notification Inbox
Catches every non-mail Laravel notification in local and staging instead of sending it, and shows each one as it would look on the other end.
What it solves
Mailpit catches your mail, but push, Slack, Teams, Telegram, Discord and SMS notifications in development are either really sent or not seen at all.
And Notification::fake() never calls your toX() methods, so a broken Slack block or a missing chat id still passes your tests.
In the environments you allow, the package cancels each non-mail notification at Laravel's NotificationSending event, builds the message with the channel's own toX() method and route, and stores it for an inbox with look-alike previews. NotificationInbox::fake() does the same in memory and gives your tests assertions on the real payloads.
- Catches web push, FCM, APNs, Expo, OneSignal, Pushover, Slack, Teams, Telegram, Discord, SMS channels and broadcast; mail and database pass through
- Look-alike previews: phone push alert, Slack Block Kit, Teams Adaptive Card, Telegram bubble, Discord embed, SMS thread with segment count
- Builds each message with the real toX() method, so a notification that would fail to send fails here first
- NotificationInbox::fake() with assertions such as assertCaptured, assertCapturedOn, assertCapturedFor and assertNoCaptureErrors
- Environment allow-list (local by default); production needs a second switch and then warns loudly
- Stored as JSON files with secret keys and webhook paths redacted, and optional masking of one-time codes in text
Install
Requires PHP 8.3+ and Laravel 12 and 13.
composer require amjadiqbal/laravel-notification-inbox --dev
Usage
use AmjadIqbal\NotificationInbox\Facades\NotificationInbox;
NotificationInbox::fake();
$user->notify(new OrderShipped($order));
NotificationInbox::assertCaptured(OrderShipped::class)
->assertCapturedOn('telegram', fn ($n) => str_contains($n->body(), 'A-1042'))
->assertCapturedContaining('Track parcel', 'slack')
->assertNoCaptureErrors();
Setup
Install it as a dev dependency, send a notification, then open /notification-inbox. Nothing to publish and no migration. By default it is active only in the local environment. If your staging server installs without dev packages and you want the inbox there, require it normally and add staging to the allow-list.
- 1 composer require amjadiqbal/laravel-notification-inbox --dev
- 2 Optional: NOTIFICATION_INBOX_ENVIRONMENTS=local,staging
- 3 Open /notification-inbox.
How it catches
Laravel fires NotificationSending before it resolves a channel's driver. For every channel not in pass_through (mail and database by default), the inbox stores the message and returns false, which cancels that send, so the channel's HTTP client, SDK and credentials are never used. Laravel stops at the first listener that returns something, so the inbox registers its listener while services are still being registered, ahead of every listener added in a provider's boot() and the app's own; only a listener another package adds in its own register(), earlier, could decide first. Queued notifications are caught in the worker the same way. Unknown channels are caught too and shown as JSON, or mapped to a preview in the channels config. Your toX() and routeNotificationFor() methods still run, since the message is built for real.
Production safety
- It only runs in the environments you list; the default is local.
- Listing production alone does nothing. It also needs NOTIFICATION_INBOX_ALLOW_PRODUCTION=true, and then it logs a warning on every request and php artisan about says so in red.
- Where the inbox is off, its routes do not exist (404).
- It fails closed: while the inbox is on, a notification is never sent, even if storing it fails.
- For channels it lets through, its listener returns null, so other NotificationSending listeners keep working.
The inbox
New notifications appear without a reload. Filter by channel or search the text, recipient and notification class. Each one has Preview, Payload and Details tabs, and previews flag problems such as an SMS that splits into several segments or a push with no title. The dashboard sits behind the viewNotificationInbox gate, and Delete and Clear all need a second gate, manageNotificationInbox; both open only in local until you define them.
use Illuminate\Support\Facades\Gate;
Gate::define('viewNotificationInbox', fn ($user) => $user?->is_admin);
Gate::define('manageNotificationInbox', fn ($user) => $user?->is_admin); // delete and clear
Personal data and retention
Each notification is stored as a JSON file in storage/notification-inbox, with a .gitignore. The notifiable is stored as its type, key and a label, never its attributes. Before writing, keys named in redact and keys containing a word from redact_containing (password, passcode, secret, token, otp, code, private_key, webhook) are masked, and webhook routes keep only their host. One-time codes inside the message text are stored as they are unless NOTIFICATION_INBOX_MASK_CODES=true, which masks every 4 to 8 digit number in the text (order numbers too, which is why it is off by default). Pruning is opportunistic: about one capture in fifty (prune_lottery) also deletes notifications older than retention_hours (72) and the oldest beyond max_messages (1000), so a quiet inbox is not pruned on its own; schedule the prune command on a shared staging server.
// routes/console.php
Schedule::command('notification-inbox:prune')->hourly();
php artisan notification-inbox:clear # delete everything
Hire me
Need this built into your app, or something custom?
I wrote Laravel Notification Inbox. I can wire it into your Laravel app, extend it for your case, or build the feature around it. Tell me what you need and you'll get a plan and a quote, free.
Support
Found a bug or want a feature? Open an issue on GitHub. For help adding it to your own project, use the quote form above.
More Laravel packages
All packages
Laravel Driver.js
Build Driver.js product tours in PHP and render them with one Blade component.
Laravel Sonner
Sonner toast notifications for Laravel, sent from PHP and shown without React.
Laravel Ninja Keys
A Cmd+K / Ctrl+K command palette for Laravel, with the actions defined in PHP.
Laravel SweetAlert2
SweetAlert2 popups, toasts and confirm dialogs from Laravel controllers, Livewire and Alpine.
Laravel Grid.js
Turn any Eloquent query into a searchable, sortable, paginated Grid.js data table with one fluent PHP call.
Laralink
Work on a Composer package inside the Laravel app that uses it, then switch back to the Packagist release.
Refresher
A drop-in replacement for Laravel's RefreshDatabase that restores a cached snapshot of your test database instead of running migrate:fresh.
Kiln
Artisan commands and an optional signed HTTP route to clear, warm and inspect OPcache when you deploy.
Laravel Media Viewer
Show private PDFs, video and audio from any Laravel disk without making the files public.
Laravel Swiper
Swiper sliders, carousels and image galleries for Laravel as Blade components.
Laravel MySQL Advisor
Read-only health checks for the MySQL or MariaDB database behind your Laravel app, with the SQL and the Laravel migration to fix a problem wherever a fix applies.
Laravel Webhook Console
A dashboard and replay tool for the webhooks spatie/laravel-webhook-client stores in your Laravel app.
Laravel Pennant Dashboard
A browser dashboard for laravel/pennant feature flags: per-scope toggles, sticky percentage rollouts, purge and an audit log.
Laravel WebPush Kit
The browser side of Laravel web push: a subscribe button with a two-step permission prompt, a service worker, and routes that save each user's subscription. Tested in Chrome so far.
Laravel Redis Advisor
Read-only health checks for the Redis or Valkey behind your Laravel cache, queues, sessions and Horizon, with the redis.conf line or Laravel config that fixes each problem.