NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Notifications for Laravel

Other packages’ notifications

Map a notification class another package sends to one of your types, and the package’s rules apply to it — preferences, mandatory flags, de-dup, quiet hours and the inbox. Or suppress it entirely:

// config/notifications.php
'interop' => [
    'map' => [
        RoundlyConsulting\Tickets\Notifications\TicketAssignedNotification::class => 'tickets.assigned',
        RoundlyConsulting\Events\Notifications\EventReminderNotification::class => 'events.reminder',
    ],
    'suppress' => [
        RoundlyConsulting\Events\Notifications\EventUpdatedNotification::class,             // every channel
        RoundlyConsulting\Tickets\Notifications\TicketNoteAddedNotification::class => ['mail'],
    ],
    'database' => 'redirect',   // redirect | keep | both
    'hold' => true,             // hold mapped mail in quiet hours
    'notifiables' => [],        // extra recipient classes
],

Or at runtime:

Notifications::interop()->map(TicketAssignedNotification::class, 'tickets.assigned');
Notifications::interop()->suppress(TicketNoteAddedNotification::class, ['mail']);
Notifications::interop()->mapped();       // class => type key
Notifications::interop()->suppressed();

The mapped type describes the notification; subjectFrom() tells the package what it is about:

final class TicketAssigned extends NotificationType
{
    public function key(): string { return 'tickets.assigned'; }
    public function category(): string { return 'tickets'; }
    public function subject(): ?string { return Ticket::class; }
    public function subjectFrom(Notification $notification): ?Model { return $notification->ticket; }
    public function window(): ?string { return 'PT15M'; }
    public function title(NotificationPayload $p): string { return __('app.ticket_assigned'); }  // when the notification's data has no "title"
}

How it works

  • A NotificationSending listener, asked per notifiable and per channel, after the notification’s own shouldSend(), at delivery time (on the worker for queued notifications). It cancels a channel or lets it through.
  • Matching uses is_a(): a host subclass of a mapped class is mapped; an exact entry beats an ancestor, and the most specific ancestor wins.
  • Only recipients — models using ReceivesNotifications, or a class in interop.notifiables — get preferences, de-dup, holds and the inbox redirect. Any other notifiable (a guest, a contact, an on-demand route) gets suppress only.
  • Fail-open: apart from suppress, any exception in the listener is reported, NotificationInteropFailed fires, and the notification goes out untouched. An interop bug must never stop a password reset.
  • De-dup is claim-first: two workers delivering two mapped notifications of one key at once send one mail.
  • A held notification is stored encrypted and released by the sweep after re-checking access and the preference; a tampered or undecryptable payload fails as payload and is never unserialized. A notification that cannot be serialized is not held — it goes out at once.
  • Mapped notifications are never digested; the doctor warns about a digestible mapped type. The package’s own mails are never touched.
  • Laravel’s Notification::fake() dispatches no NotificationSending, so interop does not run under it.

Decision table (per notifiable × channel)

CaseResult
The class is suppressed (all channels, or this one)cancelled
Not mapped; not a recipient or an on-demand route; a channel other than mail / databaseuntouched
database, redirect (default)An inbox item through the collapse step (title = the notification’s toDatabase()/toArray() title, else the type title); Laravel’s row cancelled.
database, keep / bothUntouched / inbox item + untouched.
mail, the subject (subjectFrom(), fresh) is not accessiblecancelled
mail, the type has a subject class but subjectFrom() returns nullPreferences and quiet hours apply; no access check, no de-dup (the doctor warns).
mail, the preference is off (not mandatory)cancelled
mail, an open delivery in the windowFolded (count++), cancelled.
mail, quiet now, type not urgent, interop.hold onHeld (encrypted payload), cancelled now, released after quiet hours.
OtherwiseA sending delivery is claimed; Laravel sends; NotificationSent → sent, NotificationFailed → failed(transport).

Time-critical guards

Anything that is_a auth-for-laravel’s AuthenticationNotification (OTPs, magic links, resets), Laravel’s ResetPassword or VerifyEmail may only map to a type that is urgent, has no window and is not digestible — otherwise a second code requested within the window would fold into the first and never arrive. Such a mapping is refused with InvalidInteropMappingException. The map is validated once every provider has booted (your types may be defined in AppServiceProvider::boot()), again lazily on first use, and by the doctor.

Ready map for Roundly packages

Suggested types for the Roundly packages’ notifications — none is auto-mapped, opt in per class. Class names are relative to RoundlyConsulting\<Package>\Notifications\ and end in Notification:

PackageClassesSuggested keyPrioritysubjectFrom()
ticketsTicketCreated, TicketAssigned, TicketReplied, TicketRequesterReplied, TicketNoteAdded, TicketMentioned, TicketStatusChanged, TicketMergedtickets.created …normal$n->ticket
ticketsTicketDueSoon, TicketOverduetickets.due_soon, tickets.overduenormal / urgent$n->ticket
eventsEventInvitation, RsvpConfirmed, WaitlistPromoted, EventUpdated, EventPostponed, EventRescheduled, EventMovedOnline, EventCancelledevents.invited …normal (cancelled: urgent)$n->event
eventsEventReminderevents.remindernormal$n->event
messagesNewMessagemessages.newnormal (window PT10M)$n->message->thread
alertsHealthCheckFailedalerts.health_check_failedurgent—
authany AuthenticationNotification (EmailOtp, MagicLink, ResetPassword, …)auth.*urgent, mandatory, no window—

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.