Ownership
Records can have an owner and deputies: who a folder, a shared mailbox or a distribution list belongs to, who may have it changed and who answers questions. The owner does not have to do the work. An assistant can request a folder for the management and handle it as a deputy.
use Anvil\Ownership\OwnerSelect;
final class FolderResource extends Resource{ protected bool $ownership = true;
public function form(Form $form): Form { return $form->schema([ TextInput::make('name')->required(), OwnerSelect::make(), ]); }}OwnerSelect chooses the owner when creating, by default the current user. It is
required and only shown when creating. Without this field the creator becomes the
owner. Someone who creates a record for another owner becomes a deputy.
The detail page shows owner and deputies and offers what the user may do. The page
My responsibilities lists the records of the user. It comes with
Access::pages(), or add ResponsibilitiesPage::class to your panel.
For lists there is a column and a filter:
$collection->columns([ TextColumn::make('name'), $this->ownerColumn(),])->filters([ $this->mineFilter(),]);Who may do what
Section titled “Who may do what”The role gives the base permission, ownership restricts it to the owner and the
deputies. It never grants a missing permission: an owner also needs a role with
folders.update.
| Change the record | Add and remove deputies | Set a new owner | |
|---|---|---|---|
| Owner | with folders.update |
yes | yes |
| Deputy | with folders.update |
yes | no |
folders.ownership (for IT) |
no | yes | yes |
folders.override |
yes | no | no |
| Others | no | no | no |
- Reading stays with the role. Owners are contacts for questions, and lists and references must keep working.
$ownerOnlynames the restricted abilities, by default['update', 'delete'].- Records without an owner can only be changed with
folders.override. - Both permissions appear in the role editor. Give them sparingly.
Deputies
Section titled “Deputies”$ownership = service('ownership');$subject = $resource->ownershipSubject();
$ownership->addDeputyUser($subject, $uuid, $userId);$ownership->addDeputyUser($subject, $uuid, $userId, validUntil: '2026-12-24');$ownership->addDeputyGroup($subject, $uuid, 'role:' . $roleUuid);$ownership->removeDeputy($subject, $uuid, Ownership::USER, (string) $userId);A deputy may do what the owner may, including decisions in workflows, except for
setting a new owner. After validUntil the deputyship ends.
Groups
Section titled “Groups”Groups come from providers. Anvil roles are the default, your application adds its own table or a directory:
final class TeamGroups implements OwnerGroupProvider{ public function key(): string { return 'team'; }
public function groups(): array { /* group => label, in the current tenant */ }
public function groupsOf(int $userId): array { /* groups of the user */ }
public function usersOf(string $group): array { /* user ids of the group */ }}public array $ownerGroupProviders = [RoleGroupProvider::class, TeamGroups::class];A group is then named team:facility. Membership is checked when it is needed, so
changes in the group apply at once.
When the owner has left
Section titled “When the owner has left”An owner who is deactivated, deleted or no member of the tenant any more has left.
anvil:tick notices this once an hour, in all tenants. It also notices an owner who
leaves through a directory.
Deputies are told with a notification, once for each owner who left. They then cannot change the record until one of them takes over:
$ownership->mayClaim($userId, $subject, $uuid);$ownership->claim($userId, $subject, $uuid);Taking over is explicit and audited, so nobody becomes responsible by correcting a typo.
Without a deputy who could take over, everyone with folders.ownership gets a
task with a link to the record. This also applies if the
deputies have left too or a group of deputies is empty. As long as a deputy could
take over, IT gets no message.
| What happens | The task |
|---|---|
| A new owner is set | closes at once |
| A deputy is added or the owner returns | closes with the next run, a new deputy is told |
| Someone completes it without a new owner | stays closed, nobody is asked again for this owner |
Nobody has folders.ownership |
no task, a warning in the log, tried again with the next run |
Finding missing responsibility
Section titled “Finding missing responsibility”A filter for lists:
->filters([$this->ownershipFilter()])| Option | Records |
|---|---|
| Without owner | no owner, or the owner has left |
| Without deputy | no deputy who has not left; expired deputies and empty groups do not count |
| Without owner and deputy | both, so nobody is responsible |
A widget for IT, in widgets() of a panel:
OwnerlessRecordsWidget::make()->limit(10)->span(12)It shows records without an owner in all resources of the panel whose owners the
user may set, with the number per resource linking to the filtered list. Users
without <slug>.ownership do not see it. Both compare against all owned records of
the resource, which gets slower with very large tables.
Existing records
Section titled “Existing records”A resource that gets ownership later has records without an owner, and these can
only be changed with folders.override. Give them an owner:
php spark anvil:ownership:assign folders --owner it@example.com --tenant acme --dry-runphp spark anvil:ownership:assign folders --owner it@example.com --tenant acmeRecords with an owner stay as they are, and every assignment is in the audit log.
Deleting records
Section titled “Deleting records”A soft deleted record keeps its owner and deputies for when it comes back. When a record is deleted for good, they go with it. Models that are not auditable have no such hook: their rows are removed once the owner has left.
In workflows
Section titled “In workflows”Actor::owner() means the active owner and all deputies:
Transition::make('approve')->from('requested')->to('approved')->by(Actor::owner());Reading it in your code
Section titled “Reading it in your code”| Method | Result |
|---|---|
of($subject, $uuid) |
owner, whether the owner is active, deputies; null without an owner |
responsible($subject, $uuid) |
user ids of the active owner and the deputies |
mayAct($userId, $subject, $uuid) |
owner or deputy, while the owner is active |
isOwner($userId, $subject, $uuid) |
the active owner |
Changes are in the audit log and raise the
domain event ownership.changed, a request for a new
owner ownership.handover.
OwnerSelect is required when creating through the API, an API client is no user.
Send the UUID of the owner as owner.
Records of a resource with ownership show it, users by UUID only:
{ "uuid": "…", "ownership": { "owner": "01a1…", "owner_active": true, "deputies": [ { "type": "user", "reference": "01a2…", "valid_until": "2026-12-24" }, { "type": "group", "reference": "role:01a3…", "valid_until": null } ] }}With the operation ownership in $api, clients with folders.ownership change it:
| Request | Body |
|---|---|
PUT /api/v1/folders/{uuid}/owner |
{"owner": "<uuid>"} |
POST /api/v1/folders/{uuid}/deputies |
{"user": "<uuid>"} or {"group": "role:<uuid>"}, optionally "valid_until": "2026-12-24" |
DELETE /api/v1/folders/{uuid}/deputies/user/<uuid> |
|
DELETE /api/v1/folders/{uuid}/deputies/group/role%3A<uuid> |
Owners and deputies must be active members of the tenant. A deputy needs an owner
first (409). A client changes responsibility, it cannot take over a record
because it is no user. Changing the content of a record needs folders.override.