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

Notifications and reminders

Every notice extends Notifications\EventNotification: ShouldQueue, sent only after the commit, on events.notifications.queue, through events.notifications.channels (mail, and database if you like), in the recipient’s locale with times in the event’s own timezone and its abbreviation. None carries a class name or another attendee’s data.

TypeClassSent whenTo
invitedEventInvitationNotificationAn invitation is sent — or, sent on a draft, once it is published (with a fresh token).The invitee (accept link; encrypted on the queue).
confirmedRsvpConfirmedNotificationAn RSVP enters going.The attendee — the check-in code as text and the ticket link, never a QR image in mail.
waitlist_promotedWaitlistPromotedNotificationA waitlisted party gets seats.The attendee — at once for a free seat; for a paid one from the payment job, with the pay link.
reminderEventReminderNotificationevents:sweep, at each reminder offset.going
updatedEventUpdatedNotificationA change of the start, end, all-day flag, timezone, venue, join link or attendance mode on an announced event.going, held
postponed · rescheduled · cancelledEventPostponed… · EventRescheduled… · EventCancelledNotificationPostponed · rescheduled · cancelled (including the holds the cancellation released).going, held, waitlisted, interested
moved_onlineEventMovedOnlineNotificationAn announced event moved online.going, held

Recipients and delivery

  • Recipients come from the audience by RSVP status (events.notifications.audiences.<type>), each once, without the person who made the change (unless notify_actor), and only those who may see the event and can be notified — a model without notification routes is skipped, and events:doctor counts them.
  • At delivery each notice checks again (shouldSend()): the event still exists and is visible to the recipient, and their RSVP still puts them in the audience — a reminder queued for someone who declined a minute later is dropped.
  • A failing notice is reported, never thrown back into the write that caused it, and a rolled-back write sends nothing. The database channel never stores a check-in code or an invitation token.
  • Mail has a subject with the event’s title, a greeting, the body lines and one button — the accept link for invited (events.urls.invitation), the ticket link for confirmed (events.urls.rsvp), the pay link for a held waitlist_promoted, else the event page (events.urls.event); no button without a URL. All-day events show the date only.

The database channel

toArray() — what database stores — is plain data, with no class names:

{
  "type": "rescheduled",
  "event": { "id": "uuid", "title": "Jazz Night", "starts_at": "2026-12-05T20:00:00+01:00", "timezone": "Europe/Bratislava", "all_day": false },
  "previous_starts_at": "2026-11-14T19:00:00+00:00",
  "reason": "The band is ill"
}
TypeExtra keys
invitedmessage
confirmedrsvp {id, status, guests}
waitlist_promotedrsvp {id, status}
reminderoffset
updatedchanged (attribute names)
postponed, cancelledreason
rescheduledprevious_starts_at, reason
moved_online—

Changing a notice

// config/events.php
'notifications' => [
    'channels' => ['mail', 'database'],
    'types' => [
        'cancelled' => App\Notifications\EventCancelledWithRefund::class,   // must extend the package's class
        'updated' => false,                                                  // off
    ],
    'audiences' => ['postponed' => ['going', 'held']],
],
use Illuminate\Notifications\Messages\MailMessage;
use RoundlyConsulting\Events\Notifications\EventCancelledNotification;

final class EventCancelledWithRefund extends EventCancelledNotification
{
    public function toMail(object $notifiable): MailMessage
    {
        return parent::toMail($notifiable)->line(__('Your ticket is refunded within 5 days.'));
    }
}

A class that is not the package’s own subtype throws, naming the key, and EVENTS_NOTIFICATIONS=off turns every notice off. The texts live in events::notifications (en, sk) — each type has a subject, a greeting and a body, plus shared lines — with the placeholders :title and :when. Override these hooks in a subclass:

HookDefault
toMail(object $notifiable): MailMessageSubject, greeting, lines(), action().
toArray(object $notifiable): arraytype, event, payload()
via(object $notifiable): arrayevents.notifications.channels
shouldSend(object $notifiable, string $channel): boolThe delivery-time re-check — keep calling parent::shouldSend().
lines() · payload() · action() (protected)The body lines · the type’s extra keys · [label, url] or null.
line(string $key, array $replace = []) · when(?CarbonImmutable $moment = null) (protected)A translated line of this type · a time in the event’s timezone and the reader’s locale.

Silent writes

Events::for($event)->silently()->edit()->title(['en' => 'Jazz Night (fixed typo)'])->save();
Events::for($event)->silently()->reschedule('2026-12-05 20:00');

A silent handle’s event-wide changes — update, postpone, reschedule, cancel, move online — dispatch the domain events, flagged silent, but send no notice. Personal confirmations — a seat confirmed, a promotion, an invitation — are not event-wide and still go to the person concerned. Over the API, ?notify=0 does the same for PATCH /events/{event}.

Reminders

// config/events.php — every event
'reminders' => [
    'offsets' => ['P1D', 'PT2H'],     // at most five, each PT5M–P31D
    'max_lateness' => 'PT1H',
    'all_day_at' => '09:00',
],

// one event's own list replaces the config
Events::for($event)->edit()->reminders(['P2D'])->save();
  • Offsets are ISO-8601 durations before the start, between PT5M and P31D and at most five — in events.reminders.offsets for every event, or an event’s own ->reminders([...]). A configured list outside the bounds throws InvalidEventsConfigurationException naming the key, and events:doctor reports it.
  • An offset’s day part is calendar arithmetic in the event’s timezone — P1D is the day before at the same local time, 23 or 25 hours of real time across a DST change — and its time part is absolute: PT2H is two hours.
  • All-day events count back from events.reminders.all_day_at (09:00 local) on their first day: P1D is 09:00 the day before, PT1H 08:00 on the day.
  • Exactly once: events:sweep claims each reminder by inserting (event, offset, starts_at) into event_reminder_deliveries, so any number of concurrent sweepers send it once. A reschedule changes starts_at, so reminders re-arm.
  • Skipped: an offset due before the current schedule was announced — no “tomorrow” reminder for an event created an hour ahead — one more than events.reminders.max_lateness (PT1H) late, anything at or after the start, and events that are not scheduled or rescheduled.
  • RemindersDispatched(Event, string $offset, int $recipients) fires, and SweepResult counts reminders and notified.

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.