usamamuneerchaudhary/filament-notifier
A powerful multi-channel notification system for FilamentPHP with template management, scheduled delivery, event triggers, user preferences via REST API, and an analytics dashboard. Supports email, SMS, Slack, Discord and push, with built-in rate limiting and open/click tracking.
Pin before you install. With 36 stars and zero dependents, this is a young package — treat the version constraint like a production dependency, not a convenience. Check compatibility first:
composer require usamamuneerchaudhary/filament-notifier --dry-run
composer show usamamuneerchaudhary/filament-notifier
Confirm the Laravel and Filament major versions it supports before committing. Then:
composer require usamamuneerchaudhary/filament-notifier
php artisan notifier:install
composer dump-autoload # required for the package facades to register
Review what the installer actually did before running it against a shared database. It publishes config/notifier.php, runs migrations, and seeds sample channels and templates. On a fresh local DB that is fine; on staging/production, diff the migrations, and delete the sample records from the Filament resources afterward so they don't show up in your users' preferences API.
Register the plugin in your panel provider — this is what gives you the dashboard, settings pages, and the channel/event/template resources:
use Usamamuneerchaudhary\Notifier\FilamentNotifierPlugin;
public function panel(Panel $panel): Panel
{
return $panel->plugins([
FilamentNotifierPlugin::make(),
]);
}
Where to look first, in order:
config/notifier.php — the events map, channel enable flags, rate limits. This file is the real API surface; read it end to end.php artisan list notifier — confirm which commands actually exist (scheduled send, analytics cleanup) rather than assuming from the README.route:list | grep notifier — the preferences API and tracking endpoints are registered by the package; verify guards and prefixes match your app.First use case — welcome email on registration. Do not start with Slack/Discord/push. Prove the whole loop (template → event → channel → delivery → record) with email first:
welcome-email, type email, and variables name, app_name.config/notifier.php:'events' => [
'user.registered' => [
'channels' => ['email'],
'template' => 'welcome-email',
],
],
use Usamamuneerchaudhary\Notifier\Facades\Notifier;
Notifier::send($user, 'user.registered', [
'name' => $user->name,
'email' => $user->email,
]);
MAIL_MAILER=log locally, check storage/logs/laravel.log, then inspect the notifications table row for status. Only after that, move the call into your registration listener.Prefer event-driven for anything business-owned; use the facade for one-offs. The events config keeps channel routing and template selection in one reviewable place, which is exactly what you want when a product manager asks "what does this notification do?". Reserve Notifier::send() for ad-hoc, admin-triggered, or test sends. Avoid scattering event keys as string literals across the codebase — wrap them in an enum or constants class, because a typo in an event key or template key fails quietly rather than loudly.
Send after the transaction commits. The classic bug with any notification package: a user is created inside a DB transaction, the notification fires, the transaction rolls back, and the recipient gets a welcome email for an account that doesn't exist. Dispatch from a queued listener with afterCommit, or call the notifier from an Event/Listener pair that runs post-commit:
class SendWelcomeNotification implements ShouldQueue
{
public $afterCommit = true;
public function handle(UserRegistered $event): void
{
app('notifier')->send($event->user, 'user.registered', [
'name' => $event->user->name,
]);
}
}
Keep template variables flat and scalar. {{name}}, {{app_name}}, {{order_id}}. Nested arrays and objects are where template engines get inconsistent, and the README doesn't promise a specific rendering behavior. Build the payload in one place per event so every call site passes the same keys — a missing variable is usually a silently blank email, not an exception.
Use dependency injection in your own services, facades at the edges. The README explicitly recommends this, and it is the right call: PreferenceService and AnalyticsService injected into constructors are trivially mockable, while the facades resolve from the container and are best confined to controllers, listeners, and Blade.
Preferences API is a product feature, not plumbing. The endpoints under /api/notifier/preferences give you a ready-made opt-in/opt-out UI backend. Pattern worth adopting:
GET /api/notifier/preferences/available once and cache the event/channel catalogue on the frontend; it changes only when you change events or channels.channels, not from a hardcoded list — new channels appear without a frontend release.403 explicitly. If an admin flips allow_override off, every PUT starts returning 403. That should render as "your administrator manages these settings", not a generic error toast.PUTs. Debounce it.Rate limiting is a safety net for bulk work, not a per-user fairness system. Before you write a backfill, import, or announcement loop, read the settings page values and chunk accordingly. The dashboard widget shows real-time minute/hour/day usage — useful during a rollout, but don't leave auto-refresh polling on in a high-traffic admin panel.
Custom channels are a clean seam. If you need Teams, webhooks, or an internal event bus, implement ChannelDriverInterface::send() and validateSettings() and keep the transport logic entirely in the driver. Put the credentials in the channel's settings record rather than in .env where possible, so ops can rotate them from the UI. Verify how the driver is registered (config map vs. discovery) against the source before you plan around it.
Analytics: opt in deliberately. Open and click tracking injects a pixel and rewrites URLs. That is the right default for marketing-style notifications and the wrong default for transactional security emails (password resets, magic links, anything with a signed URL). Enable track_opens/track_clicks per deployment with that distinction in mind, and schedule the retention job:
// routes/console.php or Kernel
$schedule->command('notifier:cleanup-analytics')->daily();
Run it once with --dry-run first to see exactly what it will anonymize.
Maturity risk is the headline gotcha. Zero dependents and 36 stars means you are among the first serious users. Pin the exact version, read the diff of every upgrade, and write your own integration tests around the send path — template rendering, channel dispatch, preference resolution. If it breaks, you are debugging source, not Stack Overflow.
Two switches per channel. Channel enable flags exist in .env/config and channel records exist in the database. A notification silently not going out is almost always one of those being off, or the template's type not matching the channel type. Check both before touching code.
Config cache will eat your event changes. After editing the events array, run php artisan config:clear. In a containerized deploy, remember config is cached at build time — a stale image will happily use the old event map.
Facades 500 immediately after install. "Class not found" on Notifier:: is the documented composer dump-autoload step. In Docker, that means rebuilding the image or running it in the container, not on the host.
Scheduling needs infrastructure you probably haven't verified. Notifier::schedule() writes intent; something must pick it up. Confirm the package ships a scheduled-send command (check php artisan list notifier), then confirm your cron runs schedule:run every minute, and that a queue worker is alive. Also check how the scheduled time is stored/compared — timezone bugs here produce notifications that appear hours late, and "late" is much harder to notice than "missing".
Tracking pixel inflates open rates. Apple Mail Privacy Protection and corporate image proxies prefetch pixels, so opens_count overstates real engagement. Treat opens as directional and clicks as the trustworthy signal. Also verify that click rewriting doesn't break unsubscribe links, signed URLs, deep links, or mailto: — test with a real client, not just a browser.
Analytics must be off in tests. A test suite that sends real mail through this package will generate tracking records, hit the tracking endpoints, and potentially trip rate limits, creating flaky tests that fail only on the fifth run. Disable analytics and rate limiting in phpunit.xml env, or fake the underlying mail transport.
Rate limits use your cache store. If the limiter is cache-backed, CACHE_STORE=array in a multi-node setup means every node has its own counter — the limits are effectively multiplied by your node count. Use Redis or the database store, and confirm the limiter is global rather than per-user (if it's global, one noisy tenant can starve everyone).
Preference API auth is your responsibility to verify. Run route:list and check which middleware guards the routes. Packages frequently assume the default web/sanctum guard; if your app uses a custom guard or multi-tenant scoping, users may read or write another tenant's preferences. Test cross-user access explicitly — this is a data leak class of bug, not a cosmetic one.
Template variable escaping is undocumented. Before you interpolate anything user-controlled into an HTML email (display names, ticket subjects, order notes), read the renderer and confirm escaping. Assume nothing until you've thrown <script> into a variable and inspected the output.
Partial channel failure is the debugging trap. A notification with channels: ['email', 'slack'] can succeed on one and fail on the other. When someone says "I didn't get it", inspect the notification record per channel, then the Laravel log — don't debug from the top-level send return value. Add monitoring/alerting on the failed-status count that the dashboard widget already exposes.
The installer is not idempotent-friendly until proven otherwise. Re-running notifier:install in CI or on a second environment is a common source of duplicate sample templates and channels. Guard it, or replace the seeded data with your own seeder.
Cleanup before go-live checklist: remove sample channels/templates, decide allow_override, set rate limits to something you've actually load-tested, schedule notifier:cleanup-analytics, turn off widget auto-refresh if the admin panel is busy, and set a calendar reminder to re-read the changelog before the next minor bump.
How can I help you explore Laravel packages today?