---
title: "Installation — Notifications for Laravel | Roundly"
description: "Install via Composer, publish both migration sets, schedule notifications:sweep every minute, set key types and make your User a recipient."
url: https://roundly-consulting.com/open-source/docs/notifications-for-laravel/installation
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Notifications for Laravel](https://roundly-consulting.com/open-source/docs/notifications-for-laravel.md)

# Installation

Require the package, publish its migrations and options-for-laravel’s (preferences are stored through it), and migrate. The service provider is auto-discovered together with the providers of the Roundly packages it builds on — crypto, enums, options and package-toolkit:

```bash
composer require roundly-consulting/notifications-for-laravel
php artisan vendor:publish --tag="notifications-migrations"
php artisan vendor:publish --tag="options-migrations"     # preferences are stored through options-for-laravel
php artisan migrate
```

The migrations are publish-only — the provider loads none of them. Five tables are created, in this order: notification\_reminders, notification\_inbox\_items, notification\_digest\_runs, notification\_deliveries and notification\_receipts. Every instant column is a dateTime and every value is UTC, so there is no MySQL 2038 limit and no DST-ambiguous wall-clock value. Laravel’s own notifications table is never touched.

Or let the install command do the publishing:

```bash
# Publishes the config and the migrations (refusing when a configured table already exists)
# and prints the schedule lines and trait snippets
php artisan notifications:install
```

## 1\. Schedule the sweep

Held mail, trailing mail, retries, digests, reminders and escalations all go out from one command. Schedule it every minute — nothing is auto-scheduled by the package:

```php
// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('notifications:sweep')->everyMinute()->withoutOverlapping(10)->onOneServer();
Schedule::command('notifications:prune')->daily();
```

withoutOverlapping(10) matters: the default mutex lives 24 hours, so a killed sweep would otherwise stall held mail, digests and reminders for a day. Each sweep step claims its rows by compare-and-set, so two schedulers never send one row twice; onOneServer() just saves work. Run a queue worker too — mail, digests and large fan-outs are queued unless notifications.queue.enabled is off.

## 2\. Settle the key types before migrating

With bigint ids everywhere the defaults fit. With UUID or ULID keys, set them before php artisan migrate:

```bash
# Before php artisan migrate — the values are frozen once the tables exist
NOTIFICATIONS_KEY_TYPE=uuid            # recipients and actors — must equal options.key_type
NOTIFICATIONS_SUBJECT_KEY_TYPE=        # blank = key_type
NOTIFICATIONS_CONTEXT_KEY_TYPE=        # blank = key_type
NOTIFICATIONS_PRIMARY_KEY_TYPE=bigint  # the package tables' own ids (bigint, uuid or ulid)
```

notifications.key\_type must equal your user key type and options.key\_type. Before any write each morph value is checked against its key type, and a mismatch throws InvalidNotificationsConfigurationException naming the config key and the model basename — never a raw SQL error.

## 3\. Make your User a recipient

A recipient is an Eloquent model that can be mailed (Laravel’s Notifiable or RoutesNotifications, or notifications.mail.address\_resolver), is Authenticatable when a type relies on the default Gate access check, and uses ReceivesNotifications:

```php
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use RoundlyConsulting\Notifications\Traits\ReceivesNotifications;

class User extends Authenticatable
{
    use Notifiable;              // mail routing (Laravel)
    use ReceivesNotifications;   // inbox(), notificationPreferences(), unreadNotificationCount()
}
```

ReceivesNotifications is named so it composes with Notifiable: no notifications(), notify() or unreadNotifications() clash. Without a mail address a delivery becomes undeliverable — never a fatal error at delivery.

## 4\. Describe a type and send it

A type needs a view policy on its subject (here PurchaseRequest), a group that lists its members (Company implementing ProvidesNotificationRecipients) and a recipient as above:

```php
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Notifications\NotificationPayload;
use RoundlyConsulting\Notifications\NotificationType;

// app/Notifications/Types/RequestAwaitingApproval.php — listed in config('notifications.types')
final class RequestAwaitingApproval extends NotificationType
{
    public function key(): string { return 'request.awaiting_approval'; }
    public function category(): string { return 'requests'; }
    public function subject(): ?string { return PurchaseRequest::class; }   // access check: Gate "view" on it
    public function window(): ?string { return 'PT5M'; }                    // 10 events in 5 min → 1 item, 1 mail

    public function title(NotificationPayload $p): string
    {
        return __('app.notifications.awaiting', ['ref' => $p->data['reference'], 'count' => $p->count]);
    }

    public function url(NotificationPayload $p): ?string { return route('requests.show', $p->subjectKey); }
    public function context(?Model $subject): ?Model { return $subject?->company; }   // "Acme s.r.o." label
}
```

Then send it from a feature — inbox, mail, preferences, de-dup, quiet hours and access are applied for you — and read the user’s inbox:

```php
use RoundlyConsulting\Notifications\Facades\Notifications;
use RoundlyConsulting\Notifications\Recipients;

Notifications::send(
    'request.awaiting_approval',
    $request,
    Recipients::role($request->company, 'approver'),
    ['reference' => $request->reference],
    actor: $request->author,
);

$items = Notifications::inbox($user)->unread()->paginate();   // InboxItemResource::collection($items)
Notifications::inbox($user)->unreadCount();                   // 3
Notifications::inbox($user)->markAllRead();
```

## Publish tags

| Tag | Publishes |
| --- | --- |
| `notifications-migrations` | Five migrations — reminders, inbox items, digest runs, deliveries, receipts — timestamped on publish. Required: the provider loads none of them. |
| `options-migrations` | options-for-laravel’s table — preferences are stored through it. Required. |
| `notifications-config` | config/notifications.php |
| `notifications-translations` | lang/vendor/notifications/{en,sk}/{mail,digest,enums,exceptions,preferences}.php |
| `notifications-views` | The digest mail’s markdown view notifications::mail.digest. |
| `notifications-routes` | routes/notifications.php — once published it loads instead of the package’s file, still behind notifications.routes.enabled. |

```bash
php artisan vendor:publish --tag="notifications-config"         # config/notifications.php
php artisan vendor:publish --tag="notifications-translations"   # lang/vendor/notifications/{en,sk}/…
php artisan vendor:publish --tag="notifications-views"          # the digest mail view
php artisan vendor:publish --tag="notifications-routes"         # routes/notifications.php — replaces the package's file
```

## Tables

| Table | Holds |
| --- | --- |
| `notification_inbox_items` | Recipient, subject, context and actor morphs, count, data, a title and url snapshot, a unique open\_key, collapse\_until, reminder\_id, read and archived timestamps. |
| `notification_deliveries` | Status, a unique open\_key, window\_ends\_at, send\_after, events\_count, digest frequency / period / run, the encrypted interop payload (hidden), notification\_id, source. |
| `notification_reminders` | Kind, type, rule, subject morph, anchor + offset + anchor\_value, due\_at, level, parent\_id, stored recipients and data, a unique active\_key, acknowledgement + acknowledger morph. |
| `notification_digest_runs` | Unique per recipient, frequency and period. |
| `notification_receipts` | Unique per key and recipient; no soft deletes — a ledger. |

## Translations and labels

notifications::mail, notifications::digest, notifications::enums, notifications::exceptions and notifications::preferences ship in English and Slovak. The labels of your types and categories live in your own translations:

```php
// lang/en/notifications.php — your type and category labels (and lang/sk/notifications.php)
return [
    'types' => [
        'request.awaiting_approval' => [
            'label' => 'Request awaiting approval',
            'description' => 'When a purchase request needs your approval.',
        ],
    ],
    'categories' => [
        'requests' => 'Requests',
    ],
];
```

## Check the setup

```bash
php artisan notifications:doctor
php artisan about --only=notifications
```

The doctor reports key-type drift, a sync queue, a non-shared preference lock store, invalid default quiet hours, types without a policy or ability, interop map problems and a user model that cannot be mailed (see Commands). The about section shows the type count, the queue (connection and queue only as SET or DEFAULT), routes, key types, the digest default and the interop counts — never a secret.

## 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/notifications-for-laravel.md)
