Skip to content

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(),
]);

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.
  • $ownerOnly names 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.
$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 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 */ }
}
app/Config/Anvil.php
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.

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

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.

A resource that gets ownership later has records without an owner, and these can only be changed with folders.override. Give them an owner:

Terminal window
php spark anvil:ownership:assign folders --owner it@example.com --tenant acme --dry-run
php spark anvil:ownership:assign folders --owner it@example.com --tenant acme

Records with an owner stay as they are, and every assignment is in the audit log.

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.

Actor::owner() means the active owner and all deputies:

Transition::make('approve')->from('requested')->to('approved')->by(Actor::owner());
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.