Skip to main content
Inly Core provides CoreNotification, an abstract base notification class that gives every notification a consistent structure and multi-channel delivery out of the box: database storage, real-time broadcasting, email, and SMS.

Quick start

Extend CoreNotification, implement the required getTitle() and getBody() methods (and configure channels in the constructor), and implement toMail() when using the mail channel.
<?php

namespace App\Notifications;

use App\Models\Lead;
use Illuminate\Notifications\Messages\MailMessage;
use Inly\Core\Notifications\CoreNotification;

class LeadAssignedNotification extends CoreNotification
{
    public function __construct(public Lead $lead)
    {
        $this->object($lead);
        $this->useInbox();
        $this->useMail();
    }

    protected function getTitle(): string
    {
        return __('You have been assigned to a lead');
    }

    protected function getBody(): ?string
    {
        return __('You have been assigned to lead: :title', ['title' => $this->lead->getObjectTitle()]);
    }

    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject(__('You have been assigned to a lead'))
            ->line($this->getBody())
            ->action(__('View lead'), $this->generateTrackableUrl());
    }
}
Dispatch it like any Laravel notification:
$user->notify(new LeadAssignedNotification($lead));

Channels

CoreNotification implements ShouldBroadcast and ShouldQueue. The via() method automatically resolves channels based on the flags you set:
ChannelFlagDefaultDescription
DatabasehiddenFromDatabase()EnabledPersists via CoreDatabaseChannel
BroadcastuseBroadcast() / withoutBroadcast()EnabledReal-time via Laravel Reverb
EmailuseMail() / withoutMail()DisabledBrevo API when BREVO_KEY is set, otherwise Laravel mail
SMSuseSms() / withoutSms()DisabledBrevo SMS or Twilio (automatic selection)
InboxuseInbox() / withoutInbox()nullControls visibility in the notification list UI

Channel selection flow

Database  ─── always, unless hiddenFromDatabase(true)
Broadcast ─── when useBroadcast AND config core.notifications.broadcast
Email     ─── when sendMail ─── BREVO_KEY set? → BrevoEmailChannel : mail
SMS       ─── when sendSms  ─── Brevo SMS configured? → BrevoSmsChannel
                              └─ Twilio configured?    → TwilioChannel

Abstract methods (required)

Every CoreNotification subclass must implement:

getTitle(): string

Return the notification title. Used for database storage, broadcast payload, mail subject, and inbox display.

getBody(): ?string

Return the notification body. Used for database storage, broadcast payload, mail/SMS content, and inbox display. When using the mail channel, also implement toMail().

toMail(object $notifiable): MailMessage

Returns the Laravel MailMessage for email delivery. Also used as the source for the Brevo email (rendered to HTML automatically).
public function toMail(object $notifiable): MailMessage
{
    return (new MailMessage)
        ->subject(__('New lead needs to be assigned'))
        ->line(__('A new lead needs to be assigned: :title', [
            'title' => $this->lead->getObjectTitle(),
        ]))
        ->action(__('View lead'), $this->generateTrackableUrl());
}

Constructor configuration

Configure channels and metadata with fluent with* methods in your constructor:
public function __construct(public Lead $lead)
{
    $this->object($lead);        // Link to a domain object
    $this->title(__('Lead assigned'));
    $this->icon('user-check');    // Lucide icon name
    $this->url(route('leads.show', $lead));
    $this->useInbox();              // Show in notification list
    $this->useMail();               // Send email
    $this->useSms();                // Send SMS
    $this->causer($assignedBy);  // Track who triggered it
}

Fluent API reference

Content methods

MethodDescription
getTitle(): stringAbstract. Return the notification title (required).
getBody(): ?stringAbstract. Return the notification body (required).
icon(?string $icon)Set a Lucide icon name
url(?string $url)Set the click-through URL
object(?ObjectContract $object)Link to a domain object (auto-sets title, URL, and icon from the object)
causer(?Model $causer)Track the model that triggered this notification

Channel toggle methods

MethodDescription
useMail(bool $sendMail = true)Enable/disable email delivery
withoutMail()Shortcut for useMail(false)
useSms(bool $sendSms = true)Enable/disable SMS delivery
withoutSms()Shortcut for useSms(false)
useInbox(bool $useInbox = true)Show/hide in the inbox UI
withoutInbox()Shortcut for useInbox(false)
useBroadcast(bool $useBroadcast = true)Enable/disable broadcasting
withoutBroadcast()Shortcut for useBroadcast(false)
hiddenFromDatabase(bool $hidden = true)Skip database storage entirely

Exclusive channel methods

Use these to disable all other channels and enable only one:
MethodDescription
onlyMail()Email only (no database, broadcast, inbox, or SMS)
onlyInbox()Inbox only (no email, broadcast, or SMS)
onlyBroadcast()Broadcast only (no email, inbox, or SMS)
onlySms()SMS only (no email, broadcast, or inbox)
disableAllChannels()Disable everything (call before selectively re-enabling)

Domain objects

object() accepts any model implementing ObjectContract. It automatically populates:
  • title from getObjectTitle()
  • URL from getObjectUrl() (if no URL is set)
  • icon from getObjectIcon() (if no icon is set)
The object is serialized via ObjectData and stored in the notification’s database payload, so the notification UI can link back to the related model.
$this->object($lead);
// Equivalent to:
// $this->title($lead->getObjectTitle());
// $this->url($lead->getObjectUrl());
// $this->icon($lead->getObjectIcon());

Trackable URLs

Use generateTrackableUrl() in your toMail() to create signed URLs that mark the notification as read when clicked, then redirect to the destination:
->action(__('View lead'), $this->generateTrackableUrl())
You can pass an optional URL to use instead of the notification’s default (set via url() or object()):
->action(__('View lead'), $this->generateTrackableUrl(route('leads.show', $this->lead)))
If neither an argument nor the notification URL is set, generateTrackableUrl() throws InvalidArgumentException. The generated URL is valid for 30 days and routes through notifications.track.

SimpleNotification

For ad-hoc notifications that don’t need a dedicated class, use SimpleNotification:
use Inly\Core\Notifications\SimpleNotification;

$user->notify(
    SimpleNotification::make(__('Your export is ready'))
        ->withBody(__('Click to download your file'))
        ->url(route('exports.download', $export))
        ->icon('download')
        ->useMail()
);
SimpleNotification extends CoreNotification and implements getTitle() / getBody() by storing values set via the fluent title() and body() setters. It also implements toMail() for you. Use SimpleNotification::make('Title') then optionally ->body('...'), mail subject, and SMS message via fluent setters.

Database storage

Unless hiddenFromDatabase(true) is set, all notifications are persisted via CoreDatabaseChannel. The channel stores:
  • Standard notification data (title, body, icon, url, object)
  • Channel flags (via_inbox, via_broadcast, via_mail, via_sms)
  • Causer information (causer_type, causer_id)
  • Subject (domain object) information (subject_type, subject_id)
  • Serialized constructor arguments (for replay/resend)
  • Rendered mail HTML in a separate DocumentBody record

Broadcasting

Notifications broadcast on the notification.received event type with this payload:
{
  "id": "uuid",
  "type": "LeadAssignedNotification",
  "title": "You have been assigned to a lead",
  "body": "You have been assigned to lead: John Doe",
  "icon": "user-check",
  "url": "/leads/42",
  "object": { "...ObjectData" },
  "created_at": "2026-02-24T12:00:00.000Z"
}
Broadcasting respects config('core.notifications.broadcast') and can be disabled per-notification with withoutBroadcast().

Real-world examples

Inbox + email notification with object

class NewLeadNotification extends CoreNotification
{
    public function __construct(public Lead $lead)
    {
        $this->object($lead);
        $this->useInbox();
        $this->useMail();
    }

    protected function getTitle(): string
    {
        return __('New lead needs to be assigned');
    }

    protected function getBody(): ?string
    {
        return __('A new lead needs to be assigned: :title', ['title' => $this->lead->getObjectTitle()]);
    }

    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject(__('New lead needs to be assigned'))
            ->line($this->getBody())
            ->action(__('View lead'), $this->generateTrackableUrl());
    }
}

Email-only notification (auto-reply)

class LeadAutoReplyNotification extends CoreNotification
{
    public function __construct(public Lead $lead)
    {
        $this->title('Auto reply');
        $this->body(__('Thank you for your inquiry.'));
        $this->onlyMail();
    }

    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject(__('Thank you for your inquiry'))
            ->greeting(__('Hello :name', ['name' => $this->lead->name ?? __('there')]))
            ->line(__('Thank you for reaching out. We have received your inquiry.'))
            ->line(__('We will get back to you as soon as possible.'))
            ->salutation(__('Best regards,') . "\n" . config('app.name'));
    }
}

Verification code (no inbox, no broadcast)

class VerifyCodeAuthenticationNotification extends CoreNotification
{
    public function __construct(public CodeAuthentication $codeAuthentication)
    {
        $this->useMail($codeAuthentication->method === CodeAuthenticationMethod::EMAIL);
        $this->useSms($codeAuthentication->method === CodeAuthenticationMethod::SMS);
        $this->withoutInbox();
        $this->withoutBroadcast();
    }

    protected function getTitle(): string
    {
        return __('Verification code');
    }

    protected function getBody(): ?string
    {
        return __('Your authentication code is: :code', ['code' => $this->codeAuthentication->code]);
    }

    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject(__('One-time code for :app', ['app' => config('app.name')]))
            ->markdown('emails.verify-authentication-code', [
                'code' => $this->codeAuthentication->code,
            ]);
    }
}

Static helper methods

MethodReturnsDescription
isBrevoEmailEnabled()booltrue when BREVO_KEY is configured
isBrevoSmsEnabled()booltrue when BREVO_KEY and BREVO_SMS_SENDER are configured
isTwilioSmsEnabled()booltrue when Twilio is enabled and has an account SID
isSmsEnabled()booltrue when any SMS provider is available
getSmsChannelClass()?stringReturns the active SMS channel class, or null

Brevo integration

CoreNotification integrates with Brevo (formerly Sendinblue) for both email and SMS delivery. Brevo is used automatically when configured - no code changes required in your notification classes.

Configuration

Add the following to your .env file:
# Required for Brevo email
BREVO_KEY=your-brevo-api-key-here

# Email sender (used by both Brevo and standard mail)
MAIL_FROM_ADDRESS=noreply@example.com
MAIL_FROM_NAME="Your App Name"

# Optional - enables Brevo SMS (otherwise Twilio is used)
BREVO_SMS_SENDER=YourSender
The config file is at config/brevo.php:
return [
    'key' => env('BREVO_KEY'),
    'emailFrom' => [
        'email' => env('MAIL_FROM_ADDRESS'),
        'name' => env('MAIL_FROM_NAME'),
    ],
    'smsFrom' => env('BREVO_SMS_SENDER'),
];

How email delivery works

When BREVO_KEY is set, useMail() routes email through BrevoEmailChannel instead of Laravel’s default mail channel. The default toBrevoEmail() implementation on CoreNotification:
  1. Calls your toMail() method
  2. Renders the MailMessage to HTML
  3. Builds a BrevoEmailMessage with the rendered content, subject, and sender
This means your existing toMail() implementation works with Brevo with zero changes.

Customizing the Brevo email

Override toBrevoEmail() to use Brevo-specific features like templates, tags, or custom params:
use YieldStudio\LaravelBrevoNotifier\BrevoEmailMessage;

public function toBrevoEmail(object $notifiable): BrevoEmailMessage
{
    return (new BrevoEmailMessage)
        ->from(config('brevo.emailFrom.name'), config('brevo.emailFrom.email'))
        ->to($this->getNotifiableName($notifiable), $notifiable->email)
        ->templateId(42)
        ->params([
            'name' => $notifiable->name,
            'lead_title' => $this->lead->getObjectTitle(),
        ]);
}

BrevoEmailMessage methods

MethodDescription
from($name, $email)Set sender
to($name, $email)Add recipient (can be called multiple times)
subject(string $subject)Set email subject
htmlContent(string $html)Set HTML content
textContent(string $text)Set plain text content
templateId(int $id)Use a Brevo template
params(array $params)Template parameters
cc($name, $email)Add CC recipient
bcc($name, $email)Add BCC recipient
replyTo($name, $email)Set reply-to address
attachment($name, $content)Add attachment
tags(array $tags)Email tags
headers(array $headers)Custom headers

How SMS delivery works

SMS channel is selected automatically:
  • Brevo SMS when both BREVO_KEY and BREVO_SMS_SENDER are configured
  • Twilio as a fallback when Brevo SMS is not configured
The default toBrevoSms() and toTwilio() implementations use getBody() as SMS content. Override them for custom SMS text.

Troubleshooting

Emails not sending via Brevo:
  1. Verify BREVO_KEY is set in .env
  2. Ensure MAIL_FROM_ADDRESS is verified in your Brevo account
  3. Check Laravel logs for BrevoException
SMS not sending:
  1. Ensure both BREVO_KEY and BREVO_SMS_SENDER are set (for Brevo)
  2. Verify sender name is approved in Brevo
  3. Use international phone format (+1234567890)