---
title: "Policy and agent scope — Tickets for Laravel | Roundly"
description: "The TicketPolicy behind the API, the Gate abilities you define, scoped managers, and an AgentScope that limits agents to the tickets they may work on."
url: https://roundly-consulting.com/open-source/docs/tickets-for-laravel/authorization
language: en
---

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

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

# Policy and agent scope

Policies\\TicketPolicy is the authority of the API — the PHP API never asks it. It is registered for the configured ticket model (tickets.authorization.register\_policy) and swappable through tickets.authorization.policy with a subclass. Its abilities rest on Gate abilities you define (tickets.agent, tickets.assign, tickets.manage; merge defaults to tickets.agent); an undefined one denies:

| Ability | Who |
| --- | --- |
| `viewAny` | Anyone signed in (the list is scoped). |
| `view` | An agent the agent scope allows, the requester, a watcher (watchers.can\_view); anyone else gets 404. |
| `create` | Anyone signed in (self\_service), agents always. |
| `update` | Agents; the requester only subject / description (requester\_can\_edit) and requester-editable fields, while unresolved. |
| `delete, restore` | The manage ability, held by an agent the agent scope lets work on the ticket. |
| `manageCategories, manageFields, manageTags` | The manage ability. |
| `transition` | Agents; the requester (a watcher too with participants\_can\_transition) only requester\_transitions. |
| `assign` | Agents with the assign ability; any agent for themselves (claim). |
| `unassign` | Agents with the assign ability, or the agent holding the ticket. |
| `reply` | Whoever may view, until Closed (409 ticket\_closed). |
| `note, viewInternal, tag, link` | Agents. |
| `updateComment, deleteComment` | Agents; anyone else only their own public reply. |
| `manageWatchers` | Agents; anyone who may view, for themselves. |
| `merge` | The merge ability, held by an agent the agent scope lets work on the ticket (asked for both tickets). |
| `viewTags` | Agents. |

Asked without attributes (@can('update', $ticket)), update answers whether the user may edit anything: the subject and description, or an active requester-editable field of the ticket’s field set.

Managers are scoped agents. Deleting or restoring a ticket needs tickets.manage and being an agent for that ticket (tickets.agent within the AgentScope); a manager without tickets.agent, or outside the scope, gets the same 404 as for a hidden ticket. Grant both abilities to people who manage tickets.

register\_policy off is not deny-all: Laravel’s policy discovery maps RoundlyConsulting\\Tickets\\Models\\Ticket to RoundlyConsulting\\Tickets\\Policies\\TicketPolicy by name and finds the packaged policy anyway. To use your own, register it with Gate::policy(Ticket::class, MyTicketPolicy::class), or simply name a subclass in tickets.authorization.policy. An invalid policy registers nothing and tickets:doctor reports it.

## Agent scope

By default every agent works on every ticket. Narrow it with a Contracts\\AgentScope:

```php
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Tickets\Contracts\AgentScope;
use RoundlyConsulting\Tickets\Models\Ticket;

final readonly class ProjectMembersOnly implements AgentScope
{
    public function allows(Model $agent, Ticket $ticket): bool
    {
        return $ticket->project_id === null
            || $agent->projects()->whereKey($ticket->project_id)->exists();
    }

    public function constrain(Builder $query, Model $agent): void
    {
        // where clauses only — they are grouped and OR-ed with the requester and watcher branches
        $query->where(fn (Builder $q) => $q
            ->whereNull('project_id')
            ->orWhereIn('project_id', $agent->projects()->select('projects.id')));
    }
}
```

```php
// config/tickets.php
'authorization' => ['agent_scope' => App\Tickets\ProjectMembersOnly::class, /* … */],
```

allows() decides one ticket; constrain() narrows a list (visibleTo, the index). They must agree — test yours as the package tests its own. constrain() also runs inside a key subquery for the agent-only filters and sort, so keep it to where clauses on the ticket table.

The scope is the authority on an agent for this ticket everywhere — the API and the PHP API alike: the policy and the resources, notifications and mentions, assignment eligibility, the author role of a message, internal notes, agent-only custom fields, the agent-only index filters and the last\_activity\_at sort, merging (both tickets), the other end of a link (reference only), and deleting and restoring. An agent outside the scope still sees a ticket they requested or watch, as requester or participant.

## 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/tickets-for-laravel.md)
