goldnead/statamic-notifications
A Statamic addon that brings a flexible notification system to your control panel, making it easy to send, manage, and display notifications for users and editors without custom code.
v1.12.0 is a small additive release: deferred delivery behaves exactly as in v1.11.0 (immediate notifications still work, deferred delivery stays opt-in), and the only functional change is how the addon's settings entry is placed in the Control Panel navigation. There is no migration and no new config key.
Minimal steps:
composer update <package>
php artisan migrate
config/notifications.php for deferred.max_attempts and backoff_seconds. In the Control Panel these settings are reachable from the addon's own navigation entry when statamic-brand-context 1.16+ is installed; on older versions the previous layout is kept.$schedule->command('notifications:dispatch-pending')
->everyMinute()
->withoutOverlapping();
notification_deliveries migration/schema.DeferredChannel implementation for in-app and mail.DeferredRecipientResolver.notifications:dispatch-pending command.notifications.deferred config block.settingsNavParent() if you are debugging where the settings screen appears in the CP.First use case: enqueue a non-urgent notification inside a business transaction, such as a follow-up reminder after signup or an order follow-up, then let the scheduled command deliver in-app and mail notifications later with retry/backoff.
Use deferred delivery when the business write must be atomic but delivery does not need to happen in the request. Call Notifications::notifyDeferred(...) inside the same transaction as the domain change so the notification and one delivery row per channel are committed together:
DB::transaction(function () use ($user, $order) {
$order->markAsAwaitingFollowUp();
Notifications::notifyDeferred($user, new FollowUpOrder($order), [
'in-app',
'mail',
]);
});
Only channels implementing DeferredChannel can carry deferred rows. In v1.12.0 that includes in-app and mail; treat other channels as unsupported unless you add the interface yourself.
Schedule the command from the host application. The addon does not schedule anything, so without a scheduler/cron entry deferred rows will accumulate:
$schedule->command('notifications:dispatch-pending')
->everyMinute()
->withoutOverlapping();
Recipients are resolved at send time through DeferredRecipientResolver. This is useful when the recipient may change between enqueue and delivery, but it means the resolver must handle deleted users, unsubscribed addresses, disabled accounts, and missing recipient data gracefully.
Monitor outcomes rather than assuming every row sends. Rows can end as accepted, skipped, retry, cancelled, or failed. Treat skipped/cancelled as normal business outcomes, retry as transient, and failed as actionable. Tune notifications.deferred.max_attempts and backoff_seconds per environment; the command frequency controls minimum delivery latency, while exponential backoff controls retry spacing.
For custom channels, implement DeferredChannel and ensure the recipient resolver returns the data your channel needs. Test both the happy path and the skipped/cancelled/failed paths before relying on it in production.
notifications:dispatch-pending is not running, deferred notifications will never send.withoutOverlapping() is a good default, but it is not a monitoring strategy. The dispatcher claims rows under a lock; check how stale claims are recovered if the command is killed mid-run.notifyDeferred() writes the notification and delivery rows in one transaction. That is good for consistency, but avoid wrapping large domain operations in the same transaction just to enqueue a notification.DeferredChannel.notifications.deferred settings:
php artisan migrate
php artisan config:clear
notification_deliveries. High-volume deferred usage can grow the table quickly; add pruning or archival if the package does not provide it.settingsNavParent() ([section, name] of its own CP nav entry), so with statamic-brand-context 1.16+ the settings appear as the sub-item "Einstellungen" beneath that entry, and addons without a nav of their own share a single "Addon-Suite" page under Settings. Older brand-context versions ignore the method and keep the previous layout — if docs and screenshots disagree, check the installed brand-context version first.php artisan notifications:dispatch-pending manually. Check recipient resolver behavior and channel support before assuming the command is broken.DeferredChannel interface, DeferredRecipientResolver, the deferred config block, and (for CP placement only) settingsNavParent(). Keep custom resolver logic deterministic and log enough context to explain skipped/cancelled outcomes.How can I help you explore Laravel packages today?