User Resource Customization
Capell’s user resource is the host surface for editing users in the Filament admin panel. Packages can extend it without replacing the resource by using two separate extension points:
UserSchemaExtenderadds form components, sidebar panels, and relation managers.UserFormExtendermutates submitted form data and runs after create/save.
Use UserSchemaExtender for UI. Use UserFormExtender only when custom fields need persistence handling.
How the bridge works
Section titled “How the bridge works”User resource bridges are package-owned integrations that add package-specific detail to the user editor. The admin package provides the host surface and global switches; each package owns its own setting, extender, relation managers, queries, and translations.
The effective load rule is:
Admin setting && package settingAdmin also exposes broader category switches for bridges that span package boundaries:
admin.enable_security_access_user_bridgeadmin.enable_content_ownership_user_bridgeadmin.enable_support_actions_user_bridge
Use these when a bridge contributes security/access panels, content ownership and publishing activity, or support actions. Keep package-owned settings in place when the package needs its own local disable switch.
Category settings are intended to be combined with package settings:
ShouldLoadUserResourceBridgeAction::run('enable_security_access_user_bridge', true) && ShouldLoadUserResourceBridgeAction::run( 'enable_login_audit_user_bridge', resolve(LoginAuditSettings::class)->enable_user_resource_bridge, );For example, Login Audit panels are shown only when both of these are enabled:
admin.enable_login_audit_user_bridgelogin_audit.enable_user_resource_bridge
The same pattern is used by:
admin.enable_publishing_studio_user_bridgeandpublishing_studio.enable_user_resource_bridgeadmin.enable_agent_bridge_user_bridgeandagent_bridge.enable_user_resource_bridge
Use Capell\Admin\Actions\Users\ShouldLoadUserResourceBridgeAction inside bridge extenders to enforce this rule.
Built-in bridges
Section titled “Built-in bridges”Login Audit
Section titled “Login Audit”The Login Audit bridge adds Security & Access to user records:
- last login
- failed login count
- password last changed date where available
- email verified date
- MFA status where available
- active sessions where available
- audited session revocation and forced password reset actions where supported
- login audit history relation manager
Publishing Studio
Section titled “Publishing Studio”The Publishing Studio bridge adds Content Ownership & Publishing Activity to user records:
- pages, posts, media, blocks, drafts, and other package-owned content created or updated by the user
- recently published content
- recently unpublished content
- scheduled content
- reverted content
- publishing history relation managers scoped to the edited user
Prefer CMS-responsibility activity over generic audit trails so support staff can answer “what did this editor touch?” quickly.
Support Actions
Section titled “Support Actions”Support action bridges add audited account-support controls to user records:
- impersonate user, only when the auth model and permissions allow it
- resend invite
- verify email
- deactivate or reactivate user
- unlock account
- force password reset
Every support action must be permission-gated and audited by the package that owns the action.
Agent Bridge
Section titled “Agent Bridge”The Agent Bridge bridge adds AI / agent activity to user records:
- tokens owned by the user
- confirmations requested or approved
- audit entries and capability usage
- token status, last-used time, and expiry
Agent Bridge panels must never expose raw token values. Show metadata only.
Schema context
Section titled “Schema context”Every user schema extender receives UserSchemaContextData:
use Capell\Admin\Data\Schemas\UserSchemaContextData;
$context->operation; // create or edit$context->record; // edited model on edit, null on create$context->roleNames; // role names resolved from the edited user$context->schemaType; // role-derived schema type$context->resourceName; // usersUse helpers when targeting roles or schema types:
if ($context->hasRole('editor') && $context->isSchemaType('editorial')) { // Add editorial user controls.}Role-to-schema-type mapping is configured in capell-admin.php:
'user_resource' => [ 'default_schema_type' => 'default', 'role_schema_types' => [ 'super_admin' => 'administrator', 'editor' => 'editorial', ],],The first configured matching role wins. When nothing matches, Capell uses default_schema_type.
Form hook points
Section titled “Form hook points”UserSchemaExtender::extendComponentsForHook() receives one of these hooks:
BeforeIdentityAfterIdentityBeforeCredentialsAfterCredentialsBeforeRolesAfterRolesBeforeProfileAfterProfileFooter
Add fields near the data they belong to. Avoid using the footer for package dashboards or record history; use sidebar panels or relation managers for that.
Creating a schema extender
Section titled “Creating a schema extender”Extend AbstractUserSchemaExtender so new methods added later do not break your package.
<?php
declare(strict_types=1);
namespace Vendor\Package\Extenders;
use Capell\Admin\Actions\Users\ShouldLoadUserResourceBridgeAction;use Capell\Admin\Data\Schemas\UserSchemaContextData;use Capell\Admin\Enums\UserSchemaHookEnum;use Capell\Admin\Support\Schemas\AbstractUserSchemaExtender;use Filament\Forms\Components\TextInput;use Filament\Schemas\Schema;use Vendor\Package\Settings\PackageSettings;
final class PackageUserSchemaExtender extends AbstractUserSchemaExtender{ public function supports(UserSchemaContextData $context): bool { return ShouldLoadUserResourceBridgeAction::run( 'enable_example_user_bridge', resolve(PackageSettings::class)->enable_user_resource_bridge, ); }
public function extendComponentsForHook(Schema $schema, UserSchemaHookEnum $hook, UserSchemaContextData $context): array { if ($hook !== UserSchemaHookEnum::AfterProfile) { return []; }
return [ TextInput::make('external_reference') ->label(__('vendor-package::form.external_reference')) ->maxLength(100), ]; }}Register the extender from your admin provider:
use Capell\Admin\Contracts\Extenders\UserSchemaExtender;use Vendor\Package\Extenders\PackageUserSchemaExtender;
$this->app->bind(PackageUserSchemaExtender::class);$this->app->tag([PackageUserSchemaExtender::class], UserSchemaExtender::TAG);Persisting custom fields
Section titled “Persisting custom fields”Schema extenders add UI only. If a field does not map directly to a column or relationship on the configured user model, pair it with UserFormExtender.
<?php
declare(strict_types=1);
namespace Vendor\Package\Extenders;
use Capell\Admin\Contracts\Extenders\UserFormExtender;use Illuminate\Database\Eloquent\Model;
final class PackageUserFormExtender implements UserFormExtender{ public function mutateDataBeforeCreate(array $data): array { return $this->moveExternalReference($data); }
public function afterCreate(Model $record): void {}
public function mutateDataBeforeSave(Model $record, array $data): array { return $this->moveExternalReference($data); }
public function afterSave(Model $record): void {}
private function moveExternalReference(array $data): array { $data['bio'] = $data['external_reference'] ?? $data['bio'] ?? null; unset($data['external_reference']);
return $data; }}Register it separately:
use Capell\Admin\Contracts\Extenders\UserFormExtender;use Vendor\Package\Extenders\PackageUserFormExtender;
$this->app->bind(PackageUserFormExtender::class);$this->app->tag([PackageUserFormExtender::class], UserFormExtender::TAG);Sidebar panels
Section titled “Sidebar panels”Use sidebar components for compact summaries. Keep row-level history in relation managers.
use Filament\Schemas\Components\Section;use Filament\Schemas\Components\Text;
public function extendSidebarComponents(Schema $schema, UserSchemaContextData $context): array{ if ($context->record === null) { return []; }
return [ Section::make(__('vendor-package::user.summary')) ->compact() ->schema([ Text::make(fn (): string => __('vendor-package::user.summary_line')), ]), ];}Do not put secrets, model IDs that users do not need, signed URLs, or raw package internals into sidebar panels.
Relation managers
Section titled “Relation managers”Use relation managers for package-owned history and activity tables. EditUser resolves user bridge relation managers through Filament’s normal relation-manager filtering path, so canViewForRecord() still applies.
use Capell\Admin\Data\Schemas\UserSchemaContextData;use Filament\Resources\RelationManagers\RelationManager;use Illuminate\Database\Eloquent\Model;
public function extendRelationManagers(Model $record, array $relationManagers, UserSchemaContextData $context): array{ return [ ...$relationManagers, PackageActivityRelationManager::class, ];}
final class PackageActivityRelationManager extends RelationManager{ public static function canViewForRecord(Model $ownerRecord, string $pageClass): bool { return auth()->user()?->can('viewPackageActivity', $ownerRecord) ?? false; }}Every relation manager must scope its query to the edited user. Add tests proving unrelated users’ records are excluded.
Package settings
Section titled “Package settings”Each bridge package should expose its own setting:
public bool $enable_user_resource_bridge = true;Register the settings class and schema through SettingsSchemaRegistry, and add a guarded settings migration:
if (! $this->migrator->exists('example.enable_user_resource_bridge')) { $this->migrator->add('example.enable_user_resource_bridge', true);}The package setting lets package owners disable only their bridge while leaving other user bridges available. The Admin setting lets site owners disable a whole bridge category globally.
Admin language preference
Section titled “Admin language preference”The user resource includes an Admin Language field when the Capell migration has added users.preferred_admin_language_id.
Options are loaded from enabled Capell Language records. The selected language is applied only inside authenticated Capell/Filament admin requests. If the selected language is disabled, deleted, missing, or has an invalid locale value, Capell falls back to config('app.locale').
The preference is persisted by Capell’s user form save flow, so host user models do not need to include preferred_admin_language_id in $fillable.
Testing checklist
Section titled “Testing checklist”For each user bridge, add focused tests that prove:
- the bridge loads only when Admin and package settings are both enabled
- sidebar components render on the edit-user form
- relation managers are present only when allowed by
canViewForRecord() - relation manager queries are scoped to the edited user
- custom form fields persist through
UserFormExtenderwhen they are not native user columns - sensitive values, such as raw Agent Bridge tokens, are not rendered