---
title: "Developer Reference"
manual: "Laravel-style notification system for TYPO3"
version: "main"
permalink: "https://docs.typo3.org/permalink/agencelex/notifications:developer-api@main"
source: "Developer/Index.rst"
rendered: "2026-10-29T01:09:24+00:00"
---

> [!TIP]
> Use the table of contents in the left panel to navigate.

# Developer Reference {#developer-api}

-   [The Notifiable Trait](https://docs.typo3.org/permalink/agencelex/notifications:the-notifiable-trait@main)
-   [Making a Model Notifiable](https://docs.typo3.org/permalink/agencelex/notifications:making-a-model-notifiable-1@main)
-   [Who Can Be a Recipient?](https://docs.typo3.org/permalink/agencelex/notifications:who-can-be-a-recipient@main)
-   [Creating a Notification](https://docs.typo3.org/permalink/agencelex/notifications:creating-a-notification-1@main)
-   [Queuing Notifications](https://docs.typo3.org/permalink/agencelex/notifications:queuing-notifications-1@main)
-   [Using the Dispatcher Directly](https://docs.typo3.org/permalink/agencelex/notifications:using-the-dispatcher-directly-1@main)
-   [On-Demand Notifications](https://docs.typo3.org/permalink/agencelex/notifications:on-demand-notifications-1@main)
-   [Practical Use Cases](https://docs.typo3.org/permalink/agencelex/notifications:practical-use-cases-1@main)
-   [Reading Database Notifications](https://docs.typo3.org/permalink/agencelex/notifications:reading-database-notifications-1@main)
-   [DatabaseNotification Model](https://docs.typo3.org/permalink/agencelex/notifications:databasenotification-model@main)
-   [Adding Custom Channels](https://docs.typo3.org/permalink/agencelex/notifications:adding-custom-channels@main)
-   [Customising Email Templates](https://docs.typo3.org/permalink/agencelex/notifications:customising-email-templates@main)

## The Notifiable Trait {#notifiable-overview}

The `Notifiable` trait is the only requirement for a class to *receive*
notifications. Add it to any PHP object — Extbase entity, plain PHP class,
anonymous class — and that object immediately becomes a valid notification
recipient.

```php
use Lex\Notifications\Domain\Model\Ability\Notifiable;

class MyModel
{
    use Notifiable;
}

// That's it. Now you can do:
$instance = new MyModel();
$instance->notify(new SomeNotification());
```

There is no registry, no database table for recipients, no configuration.
Any object with the trait can be passed to the dispatcher.

## Making a Model Notifiable {#making-a-model-notifiable}

For **email delivery**, also add `HasRouteNotificationForMail`. This trait
provides the required `routeNotificationForMail()` method, which the email
channel calls to resolve the recipient's email address (remove it if the model
never receives emails). Only `getEmail()` is
required; `getFirstName()` and `getLastName()` are optional and, when
present, are used to build the recipient name (`"John Doe" <john.doe@example.com>`):

```php
use Lex\Notifications\Domain\Model\Ability\HasRouteNotificationForMail;
use Lex\Notifications\Domain\Model\Ability\Notifiable;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;

class FrontendUser extends AbstractEntity
{
    use Notifiable;
    use HasRouteNotificationForMail;

    // Required when using HasRouteNotificationForMail
    public function getEmail(): string { return $this->email; }

    // Optional. No need to define them
    public function getFirstName(): ?string { return $this->firstName; }
    public function getLastName(): ?string { return $this->lastName; }
}
```

The `Notifiable` trait exposes three methods:

```php
// Dispatches via Symfony Messenger if the notification implements ShouldQueue
$user->notify(new OrderConfirmed($order));

// Always sends immediately, ignoring ShouldQueue.
// The optional second argument overrides the channels returned by via().
$user->notifyNow(new OrderConfirmed($order));
$user->notifyNow(new OrderConfirmed($order), [NotificationChannel::CHANNEL_DATABASE]);

// Returns the route (address, webhook URL, …) of the notifiable for a channel.
// Used by channels; see "Routing" below.
$user->routeNotificationFor(NotificationChannel::CHANNEL_MAIL, $notification);
```

### Routing {#routing}

A channel asks the notifiable *where* to deliver by calling
`routeNotificationFor(string $channel, Notification $notification)`. The
`Notifiable` trait resolves it by convention:

-   `database` → `true` (there is no route: the database channel uses the
    notifiable's UID instead).
-   any other channel → calls `routeNotificationFor<Channel>($notification)`
    if the method exists (e.g. `routeNotificationForMail()`,
    `routeNotificationForSlack()`), otherwise returns `null`.

**A channel without a route is skipped.** Before delivering, the sender asks
each notifiable for its route on every channel returned by `via()`. When the
route is empty (`null`, `''`, `false`…), that channel is silently skipped
for that notifiable; the other channels and recipients are still delivered.
So every notifiable that may receive a notification through a channel must
implement the matching `routeNotificationFor<Channel>()` method (or use a
trait such as `HasRouteNotificationForMail`).

```php
class FrontendUser extends AbstractEntity
{
    use Notifiable;
    use HasRouteNotificationForMail;

    public function routeNotificationForSlack(?Notification $notification = null): string
    {
        return $this->slackWebhookUrl;
    }
}
```

The method name is built by converting the channel key to UpperCamelCase,
with `-`, `_` and spaces treated as word separators:

| Channel key | Route method |
| --- | --- |
| `mail` | `routeNotificationForMail()` |
| `slack` | `routeNotificationForSlack()` |
| `my-channel` / `my_channel` | `routeNotificationForMyChannel()` |
| `SlackChannel` (short class name) | `routeNotificationForSlackChannel()` |

## Who Can Be a Recipient? {#recipient-examples}

Because the only requirement is the `Notifiable` trait, the recipient can
be anything. Here are common patterns.

**Frontend user notifies another frontend user**

A collaboration feature where one user shares content with another:

```php
// In a frontend plugin action
$sender = $this->notifiableFrontendUserRepository->findByUid($senderUid);
$recipients = $this->notifiableFrontendUserRepository->findByUids($recipientUids);
$this->notificationDispatcher->send($recipients, new ContentSharedWithYou($page, $sender));
```

**Extension notifies a backend user**

A Scheduler task or service that alerts an admin when a background job fails:

```php
// An inline class wrapping a backend user record — no persistent domain model needed
$admin = new class($backendUserRecord['email'], $backendUserRecord['realName']) {
    use Notifiable;
    use HasRouteNotificationForMail;

    protected string $firstName;
    protected string $lastName;

    public function __construct(protected readonly string $email, string $fullName) {
        $parts = explode(' ', trim($fullName), 2);
        $this->firstName = $parts[0] ?? '';
        $this->lastName  = $parts[1] ?? '';
    }
    public function getEmail(): string { return $this->email; }
    public function getFirstName(): ?string { return $this->firstName; }
    public function getLastName(): ?string { return $this->lastName; }
};
$admin->notifyNow(new SchedulerJobFailed($taskName, $errorMessage));
```

**Inline notifiable (no database record needed)**

Send a one-off notification to any email address without a domain model:

```php
$recipient = new class('info@example.com') {
    use Notifiable;
    use HasRouteNotificationForMail;

    public function __construct(protected readonly string $email) {}
    public function getEmail(): string { return $this->email; }
};

$recipient->notifyNow(new ContactFormReceived($formData));
```

> [!TIP]
> For this kind of one-off recipient, an on-demand notification is even
> shorter — no class to write:
>
> ```php
> $this->notificationDispatcher
>     ->route(NotificationChannel::CHANNEL_MAIL, 'info@example.com')
>     ->notifyNow(new ContactFormReceived($formData));
> ```
>
> See [On-Demand Notifications](https://docs.typo3.org/permalink/agencelex/notifications:on-demand-notifications@main).

**Multiple recipients of different types in one call**

The dispatcher accepts an array of notifiables — they do not need to be the
same class:

```php
$team = [$teamMemberA, $teamMemberB, ...];

$this->notificationDispatcher->send(
    $team,
    new ImportantAnnouncement($text),
);
```

Each recipient's `via()` result can differ, so the notification class can
adapt channels based on the notifiable type:

```php
public function via(object $notifiable): array
{
    // Only store in-DB for actual frontend user records
    if ($notifiable instanceof NotifiableFrontendUser) {
        return [NotificationChannel::CHANNEL_MAIL, NotificationChannel::CHANNEL_DATABASE];
    }

    return [NotificationChannel::CHANNEL_MAIL];
}
```

## Creating a Notification {#creating-a-notification}

Extend the abstract `Notification` class and implement the methods for each
channel your notification uses:

```php
namespace MyVendor\MyExtension\Notification;

use Lex\Notifications\Notification;
use Lex\Notifications\NotificationChannel;
use Lex\Notifications\NotificationLevel;
use TYPO3\CMS\Core\Mail\MailMessage;

final class OrderConfirmed extends Notification
{
    public function __construct(
        private readonly Order $order,
    ) {}

    /**
     * RFC 5424 severity level for this notification.
     * Optional - Only if you want to specify a notification level different from INFO
     */
    public function getLevel(): int
    {
        return NotificationLevel::LEVEL_INFO;
    }

    /**
     * Which channels to use. Receives the notifiable so you can adapt
     * the channel list per recipient type/preference.
     *
     * @return string[]
     */
    public function via(object $notifiable): array
    {
        return $notifiable->prefers_sms ? ['vonage'] : [NotificationChannel::CHANNEL_MAIL, NotificationChannel::CHANNEL_DATABASE];
    }

    /**
     * Optional - Payload for the email channel.
     * Build your own MailMessage if you want.
     */
    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage())
            ->subject('Order #' . $this->order->getNumber() . ' confirmed')
            ->html('<p>Thank you! Your order is being processed.</p>')
            // Optional: when no recipient is set, the email channel uses
            // $notifiable->routeNotificationFor('mail', $this)
            ->to($notifiable->routeNotificationForMail($this));
    }

    /**
     * Optional - Payload for the database channel.
     * Returned array is JSON-encoded and stored as-is.
     *
     * @return array<string, mixed>
     */
    public function toDatabase(object $notifiable): array
    {
        return [
            'level'    => $this->getLevel(),
            'subject'  => 'Order #' . $this->order->getNumber() . ' confirmed',
            'message'  => 'Your order has been received and is being processed.',
            'order_id' => $this->order->getUid(),
        ];
    }

    /**
     * Custom channel - Payload for the Slack channel.
     */
    public function toSlack(object $notifiable): SlackMessage
    {
        return (new SlackMessage)
            ->...
            ->...
            ->...;
    }
}
```

Each channel returned by `via()` must also have a route on the notifiable
(see [Routing](https://docs.typo3.org/permalink/agencelex/notifications:routing@main)), otherwise it is skipped for that notifiable.

Only implement the channel methods you actually use. If your notification only
sends email, there is no need for `toDatabase()`. Calling a channel whose
`to…()` method is missing throws a
`TYPO3\CMS\Core\Utility\Exception\NotImplementedMethodException`.

By default (when `via()` is not overridden), a notification is sent through
the `database` and `mail` channels.

## Queuing Notifications {#queuing-notifications}

Implement the `ShouldQueue` marker interface to have your notification
dispatched asynchronously via Symfony Messenger:

```php
use Illuminate\Contracts\Queue\ShouldQueue;

final class OrderConfirmed extends Notification implements ShouldQueue
{
    // No extra methods needed — the interface is a marker only.
}
```

When `ShouldQueue` is implemented, calling `notify()` wraps the
notification in a `NotificationQueued` Messenger message. A CLI worker must
be running to process the queue:

```bash
vendor/bin/typo3 messenger:consume async --time-limit=3600
```

Call `notifyNow()` or `sendNow()` to bypass the queue and deliver
immediately regardless of `ShouldQueue`.

## Using the Dispatcher Directly {#using-the-dispatcher-directly}

Inject `NotificationDispatcherInterface` into any service, controller, or
plugin. This is the recommended approach when you do not have a direct
reference to a notifiable object, or when you need to send to multiple
recipients. The interface is implemented by `NotificationManager` and exposes:

| Method | Description |
| --- | --- |
| `send($notifiables, $notification)` | Sends to one notifiable or an array of notifiables. Queued through Symfony Messenger if the notification implements `ShouldQueue`. |
| `sendNow($notifiables, $notification, ?array $channels = null)` | Sends immediately, ignoring `ShouldQueue`. `$channels` overrides `via()` for every notifiable. |
| `channel(?string $name = null)` | Returns a channel instance (the `mail` channel when no name is given). Throws `InvalidArgumentException` for an unknown channel. |
| `route(string $channel, mixed $route)` | Starts an on-demand notification. See [On-Demand Notifications](https://docs.typo3.org/permalink/agencelex/notifications:on-demand-notifications@main). |
| `routes(array $routes)` | Starts an on-demand notification on several channels at once. |

```php
use Lex\Notifications\NotificationDispatcherInterface;

final class OrderService
{
    public function __construct(
        private readonly NotificationDispatcherInterface $notifications,
        private readonly FrontendUserRepository $userRepository,
    ) {}

    public function completeOrder(Order $order): void
    {
        $buyer = $this->userRepository->findByUid($order->getBuyerUid());

        // Dispatches via Messenger queue if ShouldQueue is implemented
        $this->notifications->send($buyer, new OrderConfirmed($order));

        // Forces immediate delivery
        $this->notifications->sendNow($buyer, new OrderConfirmed($order));
    }
}
```

Send to a batch of recipients in one call:

```php
$subscribers = $this->frontendUserRepository->findByNewsletterGroup($groupId);

$this->notifications->send(
    $subscribers->toArray(),
    new MonthlyNewsletter($content),
);
```

The dispatcher iterates each notifiable independently, so a failed delivery
for one recipient does not block the others.

You can also target a single specific channel for one call using `channel()`:

```php
// Deliver only via the database channel, regardless of what via() returns
$this->notificationDispatcher->channel(NotificationChannel::CHANNEL_DATABASE)
    ->send($user, new InvoicePaid($invoice));
```

> [!NOTE]
> `channel(...)->send()` talks to the channel directly: `via()` is not
> called and `ShouldQueue` is ignored, so delivery is always immediate.

## On-Demand Notifications {#on-demand-notifications}

Sometimes the recipient is not a model at all: a guest who left an email
address, a support mailbox, a Teams room. Instead of writing a throwaway
class, ask the dispatcher for an **anonymous notifiable** and give it a route
for each channel:

```php
use Lex\Notifications\NotificationChannel;
use Lex\Notifications\NotificationDispatcherInterface;
use Symfony\Component\Mime\Address;

public function __construct(
    private readonly NotificationDispatcherInterface $notificationDispatcher,
) {}

// One channel
$this->notificationDispatcher
    ->route(NotificationChannel::CHANNEL_MAIL, 'guest@example.com')
    ->notify(new OrderReceiptEmail($order));

// Several channels: chain route() calls
$this->notificationDispatcher
    ->route(NotificationChannel::CHANNEL_MAIL, new Address('support@example.com', 'Support'))
    ->route('slack', '#orders')
    ->notify(new OrderReceived($order));

// Several channels at once: routes()
$this->notificationDispatcher
    ->routes([
        NotificationChannel::CHANNEL_MAIL => 'support@example.com',
        'slack' => '#orders',
    ])
    ->notifyNow(new OrderReceived($order));
```

`route()` and `routes()` return a `Lex\Notifications\AnonymousNotifiable`.
It uses the `Notifiable` trait, so `notify()` (queued if `ShouldQueue`)
and `notifyNow()` (immediate) work as usual, and so does
`$this->notificationDispatcher->send($anonymous, ...)`.

Things to know:

-   **The route is whatever the channel expects.** For `mail`, it can be a
    string or a `Symfony\Component\Mime\Address`. For a custom channel, it
    can be any value (webhook URL, room ID, phone number…).
-   **via() still decides.** Only the channels returned by the notification's
    `via()` are used. A routed channel that `via()` doesn't return is
    ignored.
-   **The database channel is not supported.** An anonymous recipient has no
    UID to store, so routing to `database` throws an
    `InvalidArgumentException`.
-   **Validation:** `route()` on the dispatcher checks that the channel is
    registered and throws an `InvalidArgumentException` if it isn't.
    `routes()` and any chained `->route()` calls don't check; an unknown
    channel only fails when the notification is sent.
-   **Queued on-demand notifications** go through Symfony Messenger like any
    other. With an asynchronous transport, routes must be serializable
    (strings, `Address` objects…).

Custom channels should read the route with
`$notifiable->routeNotificationFor('<channel>', $notification)`. That works
for both anonymous notifiables and models: on a model, the `Notifiable`
trait forwards the call to `routeNotificationFor<Channel>()` (see
[Routing](https://docs.typo3.org/permalink/agencelex/notifications:routing@main)).

## Practical Use Cases {#practical-use-cases}

**Workflow approval alert to a backend user**

```php
// In a DataHandler hook or custom service
$this->notifications
    ->route(NotificationChannel::CHANNEL_MAIL, 'editor@example.com')
    ->notifyNow(new ContentPendingReview($pageUid, $submitter));
```

**Frontend user triggers a notification to another frontend user**

```php
// In a frontend plugin action (e.g. a messaging feature)
$sender   = $this->notifiableFrontendUserRepository->findByUid($senderUid);
$receiver = $this->notifiableFrontendUserRepository->findByUid($receiverUid);

$receiver->notify(new NewMessageReceived($sender, $messageText));
```

**Extension notifies multiple channels for different severity levels**

```php
final class PaymentFailed extends Notification
{
    public function via(object $notifiable): array
    {
        // Critical failures go to mail + database + Slack
        return [
            NotificationChannel::CHANNEL_MAIL,
            NotificationChannel::CHANNEL_DATABASE,
            'slack',
        ];
    }
}
```

**Sending to a plain email without any domain model**

```php
// On-demand: no class needed
$this->notifications
    ->route(NotificationChannel::CHANNEL_MAIL, 'customer@example.com')
    ->notifyNow(new OrderReceiptEmail($order));

// Or with an inline class, if you need more than an address
$contact = new class('customer@example.com') {
    use \Lex\Notifications\Domain\Model\Ability\Notifiable;
    use \Lex\Notifications\Domain\Model\Ability\HasRouteNotificationForMail;
    public function __construct(protected readonly string $email) {}
    public function getEmail(): string { return $this->email; }
};

$contact->notifyNow(new OrderReceiptEmail($order));
```

**Alerting a support mailbox and a Teams room at once**

```php
$this->notifications
    ->routes([
        NotificationChannel::CHANNEL_MAIL => new Address('support@example.com', 'Support'),
        'teams' => 'https://example.webhook.office.com/…',
    ])
    ->notify(new PaymentFailed($order));
```

## Reading Database Notifications {#reading-database-notifications}

Inject `DatabaseNotificationRepository` to query stored notifications
for the currently logged-in frontend user:

```php
use Lex\Notifications\Domain\Repository\DatabaseNotificationRepository;

class NotificationController extends ActionController
{
    public function __construct(
        private readonly DatabaseNotificationRepository $notificationRepository,
    ) {}

    public function indexAction(): ResponseInterface
    {
        $uid = $this->getContext()->getAspect('frontend.user')->get('id');

        $this->view->assign(
            'notifications',
            $this->notificationRepository->findByNotifiable($uid),
        );
        return $this->htmlResponse();
    }

    public function markAllReadAction(): ResponseInterface
    {
        $uid = $this->getContext()->getAspect('frontend.user')->get('id');
        $this->notificationRepository->markAllAsReadForNotifiable($uid);
        return $this->redirect('index');
    }
}
```

Available repository methods:

| Method | Description |
| --- | --- |
| `findByNotifiable(int $uid)` | Returns all notifications for a notifiable UID, ordered by creation date descending. |
| `markAllAsReadForNotifiable(int $uid)` | Sets `read_at` to the current timestamp for every unread notification belonging to that UID. |
| `removeAllForNotifiable(int $uid)` | Permanently deletes all notifications for that UID. |

## DatabaseNotification Model {#database-notification-model}

Each stored notification exposes:

| Getter | Type | Description |
| --- | --- | --- |
| `getType()` | string | Fully-qualified notification class name. |
| `getLevel()` | int | RFC 5424 severity level. |
| `getData()` | string | Raw JSON payload as stored by `toDatabase()`. |
| `getDataAsArray()` | array | Decoded payload as a PHP array. |
| `getReadAt()` | \\DateTime\|null | Read timestamp, or `null` if unread. |
| `getCreatedAt()` | \\DateTime | Creation timestamp. |
| `getDiffCreatedAtForHumans()` | string | Human-readable relative time (e.g. "3 minutes ago"). |
| `markAsRead()` | void | Sets `readAt` to the current date/time. |

## Adding Custom Channels {#custom-channels}

**Step 1 — implement the interface**

```php
namespace MyVendor\MyExtension\Notification\Channel;

use Lex\Notifications\Channel\ChannelInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
use Lex\Notifications\Notification;

#[AutoconfigureTag('notifications.channel')]
final class SlackChannel implements ChannelInterface
{
    public function __construct(
        private readonly SlackClient $slack,
    ) {}

    /**
     * Optional. Provides a short string key used in via().
     * When omitted, the short class name (SlackChannel) is used as the key.
     */
    public function getName(): string
    {
        return 'slack';
    }

    public function send(object $notifiable, Notification $notification): void
    {
        $payload = $notification->toSlack($notifiable);

        // Works for models (routeNotificationForSlack()) and on-demand recipients
        $webhookUrl = $notifiable->routeNotificationFor('slack', $notification);

        $this->slack->post($webhookUrl, $payload);
    }
}
```

**Step 2 — return the key from** `via()`

```php
public function via(object $notifiable): array
{
    return ['slack', NotificationChannel::CHANNEL_MAIL];
}
```

The `NotificationManager` resolves the channel by its key at send time.
If `getName()` is not defined, use the short class name (without namespace)
as the key in `via()`:

```php
public function via(object $notifiable): array
{
    return ['SlackChannel'];
}
```

**Step 3 — implement the route in each notifiable**

Every notifiable that may be notified through the channel must tell where to
deliver, with a `routeNotificationFor<Channel>()` method (see [Routing](https://docs.typo3.org/permalink/agencelex/notifications:routing@main)):

```php
use Lex\Notifications\Notification;

public function routeNotificationForSlack(?Notification $notification = null): string
{
    return 'https://hooks.slack.com/services/your/slack/webhook';
}
```

If this route is missing (or empty), the channel is skipped when the
notification is sent to that notifiable. On-demand recipients get their route
from `route()` / `routes()` instead (see [On-Demand Notifications](https://docs.typo3.org/permalink/agencelex/notifications:on-demand-notifications@main)).

> [!NOTE]
> Dependency injection works normally. Declare constructor arguments as
> usual and they will be autowired by the Symfony DI container.

Channel key lookup order:

1.  `getName()` — if the method exists on the channel class.
1.  Short class name (e.g. `SlackChannel`) — as a fallback.

> [!NOTE]
> Two channels without `getName()` sharing the same short class name in
> different namespaces would get the same key; the last one registered wins.
> Define `getName()` to avoid the collision.

To ensure a custom channel is properly detected and registered into the manager's iterator, you must assign the `notifications.channel` tag to your class.
Implementing `ChannelInterface` alone is not enough in a third-party
extension: Symfony's `_instanceof` rules only apply to the services of the
file that declares them.

You can achieve this in **one of three ways**:

**Option A: Declaration in** `Services.yaml`

Add your concrete channel class manually with the required tag in your extension's [`Configuration/Services.yaml`](https://docs.typo3.org/m/typo3/reference-coreapi/13.4/en-us/ExtensionArchitecture/FileStructure/Configuration/ServicesYaml.html#file-extension-configuration-services-yaml):

```yaml
services:
    Lex\Notifications\MicrosoftTeams\Channel\TeamsChannel:
        tags: ['notifications.channel']
```

**Option B: Using** `#[AutoconfigureTag]` **in the channel class (Recommended)**

Keep your YAML file clean by adding the Symfony attribute directly above your class definition:

```php
use Lex\Notifications\Channel\ChannelInterface;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

#[AutoconfigureTag('notifications.channel')]
final class TeamsChannel implements ChannelInterface
{
    // ... Your channel logic
}
```

**Option C: Using** `#[Autoconfigure]` **in the channel class**

If you need to change other service options (like visibility) while registering the tag, you can pass the tags directly into the `#[Autoconfigure]` attribute:

```php
use Lex\Notifications\Channel\ChannelInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

#[Autoconfigure(public: true, tags: ['notifications.channel'])]
final class TeamsChannel implements ChannelInterface
{
    // ... Your channel logic
}
```

## Customising Email Templates {#email-templates}

Email templates live in:

```none
Resources/Private/
├── Layouts/Email/
│   ├── NotificationLayout.html   # HTML wrapper
│   └── NotificationLayout.txt    # Plain-text wrapper
└── Templates/Email/
    ├── BackendUserSentMessageToFrontendUser.html
    └── BackendUserSentMessageToFrontendUser.txt
```

Override them in your site package by adjusting the Fluid template paths.
The built-in templates receive these variables:

| Variable | Type | Description |
| --- | --- | --- |
| `{level}` | int | Notification severity level. |
| `{subject}` | string | Message subject/title. |
| `{message}` | string | Message body (may contain HTML). |
| `{link}` | string\|null | Optional call-to-action URL or typolink string. |

For your own `Notification` subclasses you can use a completely different
template — simply return the rendered HTML from your `toMail()` method using
whatever rendering approach fits your extension.
