---
title: "Recipients and access — Notifications for Laravel | Roundly"
description: "Recipients::users, role, members, owner, using and merge, the group resolver and teams recipe, fail-closed access checks and revoke()."
url: https://roundly-consulting.com/open-source/docs/notifications-for-laravel/recipients-and-access
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)

# Recipients and access

Recipients resolve at send time with the subject — no email address is ever hard-coded:

```php
use RoundlyConsulting\Notifications\Recipients;

Recipients::users($anna, $boris, $collection, null);       // explicit models; nulls dropped
Recipients::role($request->company, 'approver');           // through the group resolver
Recipients::members($team);                                 // every member
Recipients::owner();                                        // HasNotificationOwner::notificationOwner()
Recipients::owner('accountManager');                        // …else this relation
Recipients::using(OnCallResolver::class, ['level' => 2]);   // your RecipientResolver, scalar args
Recipients::merge(Recipients::owner(), Recipients::role($company, 'manager'));

// A plain model, a collection or an array works wherever recipients are expected
Notifications::send('security.alert', null, $user);
Notifications::send('security.alert', null, User::query()->where('is_admin', true)->get());
```

| Spec | Resolution | Storable |
| --- | --- | --- |
| `Recipients::users($a, $b, $collection, null)` | The models; nulls dropped, a non-model throws InvalidRecipientException. | ✓ |
| `Recipients::role($group, 'approver')` | The group resolver (notifications.recipients.group\_resolver), members with the role. The group may be a closure of the subject. | ✓ (not with a closure group) |
| `Recipients::members($group)` | The same, every member. | ✓ (not with a closure group) |
| `Recipients::owner(?string $relation)` | HasNotificationOwner::notificationOwner(), else the named relation, else NoOwnerException. | ✓ |
| `Recipients::using(MyResolver::class, ['level' => 2])` | MyResolver::resolve(?Model $subject, array $args), resolved from the container; arguments must be scalars or lists of them. | ✓ |
| `Recipients::merge(...$specs)` | The union. | when every part is |
| `fn (Contract $c) => Recipients::role($c->company, 'manager')` | A closure evaluated with the subject. | ✗ — only inside code-defined reminder rules |

Storable specs survive in a reminder row, an escalation or a queued fan-out; storing a closure throws UnstorableRecipientsException.

## Your own resolver

```php
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Notifications\Contracts\RecipientResolver;

final class OnCallResolver implements RecipientResolver
{
    public function __construct(private Rota $rota) {}   // resolved from the container

    public function resolve(?Model $subject, array $args): iterable
    {
        return $this->rota->onCall(level: $args['level'] ?? 1);
    }
}
```

## The group resolver and the teams recipe

The default ContractGroupResolver needs the group to implement ProvidesNotificationRecipients; anything else throws UnresolvableRecipientsException naming the group’s basename. Bind your own ResolvesGroupMembers through notifications.recipients.group\_resolver — with teams-for-laravel (suggested, not required):

```php
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Notifications\Contracts\ResolvesGroupMembers;
use RoundlyConsulting\Teams\Facades\Teams;
use RoundlyConsulting\Teams\Models\Member;

final class TeamsGroupResolver implements ResolvesGroupMembers
{
    public function members(Model $group, ?string $role = null): iterable
    {
        return Teams::for($group)->members()->all()
            ->reject->isExpired()                     // all() includes expired memberships
            ->filter(fn (Member $member): bool => $role === null || $member->role === $role)
            ->map->member;
    }
}

// config/notifications.php
'recipients' => ['group_resolver' => App\Notifications\TeamsGroupResolver::class],
```

## Access checks

- A type without a subject() and a send without a subject: always allowed.
- Otherwise canAccess($recipient, $subject); the default asks the Gate ability notifications.access.ability (view) on the subject, for the recipient.
- Fail-closed and loud: when the default is used and the subject has neither a policy nor the ability, the send throws MissingAccessCheckException naming the type key and the subject basename. Override canAccess() (or ->access() on a fluent type) to opt out.
- It runs at every hop, always with a fresh subject (soft-deleted counts as gone unless allowsTrashedSubject()): the send, each fan-out job, mail delivery (queued, trailing, held), every digest entry, every reminder fire, every escalation, the interop listener and the release of a held interop mail. A recipient who lost access gets nothing.

```php
// The default check — the Gate ability notifications.access.ability ('view') on the subject:
Gate::forUser($recipient)->inspect('view', $subject)->allowed();

// Your own, on a class type…
public function canAccess(Model $recipient, ?Model $subject): bool
{
    return $subject?->company->users()->whereKey($recipient->getKey())->exists() ?? true;
}

// …or a fluent one
Notifications::define('announcement.posted')->subject(Announcement::class)->access(fn ($recipient, $subject): bool => true);
```

## Revoking access

Inbox reads are not re-checked by default — the inbox shows what was delivered. When access is removed, revoke it:

```php
// Someone left the company: remove their items about this request and cancel their pending mail
Notifications::for($request)->revoke($formerMember);

// For everyone — also cancels the subject's pending reminders and escalations
Notifications::for($request)->revoke();
```

Or turn on notifications.inbox.recheck\_on\_read, which hides failing items per page (counts stay raw).

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