---
title: "Inbox — Notifications for Laravel | Roundly"
description: "Collapsing within a window, contexts, immutable queries and counts, read/archive/delete, resources without class names, broadcasting and the inbox channel."
url: https://roundly-consulting.com/open-source/docs/notifications-for-laravel/inbox
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)

# Inbox

The package keeps its own inbox table — one item per recipient, type, subject and context per window, with a count and the latest actor.

## Collapsing

The same type + subject + context + recipient within the type’s window() updates one item instead of adding another: count goes up, the latest actor and data replace the old ones, and a read item becomes unread again. The window is fixed from the first event, so a chatty subject starts a fresh item every window rather than collapsing forever.

- No window → every event is its own item.
- An archived or deleted item stops collapsing: the next event starts a new item (an unarchived item stays closed).
- Context is part of the key: the same subject in two contexts stays two items.
- Escalation sends use their own lane: a recipient who got the original receives the escalation as a separate item and mail.
- Lookups include soft-deleted rows, so a deleted item never blocks the next event.

## Queries and counts

```php
$inbox = Notifications::inbox($user);               // or $user->inbox()

$inbox->paginate(perPage: 20);                       // not archived, newest activity first
$inbox->cursorPaginate();
$inbox->get();                                       // capped at inbox.max_per_page
$inbox->first();
$inbox->find($id);                                   // another user's id → InboxItemNotFoundException
$inbox->unread()->paginate();                        // read(), archived(), withArchived()
$inbox->in($company)->paginate();                    // or a key string: in('company:3')
$inbox->withoutContext();                            // allContexts() is the default
$inbox->category('requests')->type('request.awaiting_approval')->since(now()->subDay())->get();
$inbox->unreadCount();                               // 7
$inbox->counts();                                    // InboxCounts { unread, total, contexts: list<ContextCount{key, label, unread}> }
```

Pages eager-load the actor and subject — no N+1 — and perPage is clamped to notifications.inbox.max\_per\_page.

## Mutations

```php
$inbox->markRead($item);                                  // also by id; acknowledges an acknowledgedOnRead() reminder
$inbox->markUnread($item);
$inbox->in($company)->markAllRead();                      // one UPDATE within the handle's filters; returns the count
$inbox->markAllRead(in: $company, category: 'requests');   // explicit arguments replace the handle's context / category
$inbox->archive($item);
$inbox->unarchive($item);
$inbox->delete($item);                                    // soft delete
```

Mutators return the handle; markAllRead() returns the count. markAllRead() honours every filter of the handle — in() or withoutContext(), category(), type() and since() — so $inbox->in($company)->markAllRead() clears that workspace only. An item of another recipient is “not found” — the same answer as a missing id, so the inbox is no existence oracle.

## Resources

InboxItemResource renders id, type, category, title, body, url (http(s) only), count, data (the type’s publicData()), actor, subject, context, priority ({value, label}), read\_at, archived\_at, first\_event\_at and last\_event\_at (ISO-8601), in the request locale. InboxCountsResource renders InboxCounts.

```php
use RoundlyConsulting\Notifications\Http\Resources\InboxItemResource;

// A page — eager-loads actors and subjects
return InboxItemResource::collection(Notifications::inbox($request->user())->unread()->paginate());

// One item: wrap it yourself — its own data field makes Laravel skip the wrapper
return ['data' => (new InboxItemResource($item))->resolve()];
```

```json
{
  "id": 12, "type": "request.awaiting_approval", "category": "requests", "title": "…", "body": null,
  "url": "https://app.test/requests/9", "count": 10, "data": {"reference": "PR-9"},
  "actor": {"id": 4, "type": "User"}, "subject": {"id": 9, "type": "PurchaseRequest"},
  "context": {"key": "company:3", "label": "Acme s.r.o.", "id": 3, "type": "Company"},
  "priority": {"value": "normal", "label": "Normal"}, "read_at": null, "archived_at": null,
  "first_event_at": "2026-11-09T10:00:00+00:00", "last_event_at": "2026-11-09T10:04:31+00:00"
}
```

## No class names over the wire

Actor, subject and context render through swappable resources. Point a key at your own JsonResource to add a name or an avatar; blank or null is the default, anything that is not an instantiable JsonResource subclass throws naming the key, and a null actor or subject renders null. There is no recipient role: no endpoint or event payload renders another user.

| Role | Config key | Receives | Default output |
| --- | --- | --- | --- |
| `actor` | `notifications.resources.actor` | the actor model | {id, type: class\_basename} |
| `subject` | `notifications.resources.subject` | the subject model | {id, type: class\_basename} |
| `context` | `notifications.resources.context` | a ContextReference (snapshot, no query) | {key, label, id?, type?} |

```php
// config/notifications.php
'resources' => [
    'actor' => App\Http\Resources\NotificationActorResource::class,
],

// app/Http/Resources/NotificationActorResource.php — receives the actor model
final class NotificationActorResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->resource->getKey(),
            'name' => $this->resource->name,
            'avatar_url' => $this->resource->avatar_url,
        ];
    }
}
```

## Broadcasting (Reverb)

The package never broadcasts; listen to its after-commit events and broadcast the resource yourself:

```php
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Notifications\Events\InboxItemCreated;
use RoundlyConsulting\Notifications\Events\InboxItemUpdated;
use RoundlyConsulting\Notifications\Http\Resources\InboxItemResource;

Event::listen([InboxItemCreated::class, InboxItemUpdated::class], function ($event): void {
    $item = $event->item->loadMissing(['actor', 'subject']);

    broadcast(new App\Events\InboxChanged(
        userId: $item->recipient_id,
        item: (new InboxItemResource($item))->resolve(),
        unread: Notifications::inbox($item->recipient)->unreadCount(),
    ));
});

// App\Events\InboxChanged: ShouldBroadcast on new PrivateChannel('App.Models.User.'.$this->userId)
// broadcastWith(): ['item' => $this->item, 'unread' => $this->unread] — the resource only, never a class name.
```

## The Laravel inbox channel

A Laravel notification of your own can land in the inbox through the same collapse step. The type’s access check and inbox preference apply; mail is not involved — use Notifications::send() for that:

```php
use Illuminate\Notifications\Notification;
use RoundlyConsulting\Notifications\Mail\InboxMessage;

final class CommentPosted extends Notification
{
    public function via(object $notifiable): array { return ['inbox', 'broadcast']; }

    public function toInbox(object $notifiable): InboxMessage
    {
        return (new InboxMessage('comment.posted'))
            ->about($this->post)
            ->by($this->author)
            ->in($this->post->blog)
            ->with(['excerpt' => $this->excerpt]);
    }
}

// Programmatically
Notifications::inboxMessage($user, new InboxMessage('comment.posted'));   // ?InboxItem
```

## When the subject is deleted

notifications.inbox.on\_subject\_deleted is keep (the default — items render from their snapshot and data), archive or delete. It runs for subjects using HasNotificationReminders; for others, call Notifications::for($subject)->revoke() yourself.

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