Skip to content

Admin Extensions

Packages extend the admin through CapellAdmin, admin bridges, tagged contracts, settings contributors, and Filament classes.

Use an admin bridge when a package contributes more than one admin concern. The bridge keeps package wiring in one class and lets Capell boot the package’s admin surface once.

use Capell\Admin\Contracts\Bridges\AdminBridge;
use Capell\Admin\Data\Bridges\AdminBridgeContextData;
use Capell\Admin\Support\Bridges\AdminBridgeRegistrar;
final class ExampleAdminBridge implements AdminBridge
{
public function register(AdminBridgeRegistrar $registrar, AdminBridgeContextData $context): void
{
$registrar->resource(ExampleResource::class, group: 'content', name: 'examples');
$registrar->page(ExampleReportPage::class);
$registrar->dashboardWidget(ExampleHealthWidget::class, DashboardEnum::SystemHealth);
$registrar->settingsClass('example', ExampleSettings::class);
$registrar->settingsSchema('example', ExampleSettingsSchema::class);
}
}

Register the bridge from the package admin provider:

CapellAdmin::registerAdminBridge('capell-app/example', ExampleAdminBridge::class);
CapellAdmin::bootAdminBridges('capell-app/example');

For one small contribution, direct CapellAdmin::contributeToAdminSurface(...) calls are still acceptable. Prefer a bridge when the package adds a resource plus settings, widgets, extenders, or user menu actions.

Register package pages through an admin bridge or directly with AdminSurfaceContributionData::page(...):

use Capell\Admin\Data\AdminSurfaceContributionData;
CapellAdmin::contributeToAdminSurface(
AdminSurfaceContributionData::page(ExampleReportPage::class),
);

The page class should provide translated navigation labels:

public static function getNavigationLabel(): string
{
return __('capell-example::navigation.example_report');
}

If the page is the package’s main settings or control page, register it as the extension page:

CapellAdmin::registerExtensionPage('capell-app/example', ExampleSettingsPage::class);

This registers the Filament page and lists the package on the Extensions management page with a direct Edit action. Extension pages do not keep their own direct sidebar item; Capell automatically adds accessible registered extension pages to the grouped Filament sub-navigation on the Extensions page.

Register resources when the package owns a model:

use Capell\Admin\Data\AdminSurfaceContributionData;
CapellAdmin::contributeToAdminSurface(
AdminSurfaceContributionData::resource(ExampleResource::class, group: 'content', name: 'examples'),
);

The group and name pair is the lookup slot other package code can use through CapellAdmin::getResource($group, $name). Use a stable name instead of relying on class names when a package wants to replace or extend a known resource slot.

Use dashboard slots rather than hardcoded admin registration:

CapellAdmin::registerDashboardWidget(ExampleHealthWidget::class, DashboardEnum::SystemHealth);

Widgets should implement Capell\Admin\Contracts\CapellWidgetContract when they participate in Capell dashboard settings.

Use the user menu registry for admin-only package shortcuts and attention counts:

CapellAdmin::registerUserMenuItem(
key: 'capell-example.notes',
label: fn (): string => __('capell-example::user-menu.notes'),
url: fn (): string => route('filament.admin.pages.example-notes'),
badge: fn (): int => ExampleNote::query()->unread()->count(),
sort: 40,
);

See User Menu Registry for the full API, badge rules, and translated package examples.

Packages can add onboarding steps to the Capell admin welcome tour:

CapellAdmin::registerWelcomeTourStep(
key: 'capell-example.feature',
title: __('capell-example::welcome.feature_title'),
description: __('capell-example::welcome.feature_description'),
element: '.capell-example-feature',
sort: 80,
);

Use stable admin-only selectors for element. Omit element for a modal step. The tour is shown on the admin dashboard and can be enabled or disabled per user from the user form.

Packages that add widgets should register them through registerDashboardWidget() and make sure Admin settings can expose their toggles. Small counters that belong inside the Capell overview widget should use registerOverviewStat() instead of a standalone widget.

For package settings screens, use SettingsSchemaRegistry directly or the bridge registrar helpers:

$registrar->settingsClass('example', ExampleSettings::class);
$registrar->settingsSchema('example', ExampleSettingsSchema::class);
$registrar->settingsMetadata(new SettingsGroupMetadata(
group: 'example',
label: 'capell-example::settings.label',
));

Settings pages can extend AbstractPackageSettingsPage and use SettingsGroupMetadata for their page label, icon, and sort.

Use tagged extenders when adding fields or actions to core resources:

$this->app->tag([ExamplePageSchemaExtender::class], PageSchemaExtender::TAG);

Common tags:

NeedTag or registry
Page form fields, tabs, sidebar components, relation managersPageSchemaExtender::TAG
Site form fields, tabs, create wizard fields, relation managersSiteSchemaExtender::TAG
Layout tabs and relation managersLayoutSchemaExtender::TAG
User fields, sidebar components, relation managersUserSchemaExtender::TAG or a UserResourceBridge
User table columns, filters, record actions, toolbar actionsUserTableExtender::TAG
Page table columns, filters, bulk actions, query changesPageTableExtender::TAG
Page header actionsPageHeaderActionExtender::TAG
Site header actionsSiteHeaderActionExtender::TAG
Site table row actionsSiteRecordActionExtender::TAG
Header actions on arbitrary resource pagesResourceHeaderActionExtender::TAG
Page title/slug field actions or after-label schemaPageTitleWithSlugInputExtender::TAG
Page edit form actions or header widgetsPageEditExtender::TAG
Page/site export modal fields and optionsPageExportExtender::TAG
Publish panel sectionsPublishPanelExtender::TAG
Media edit header actionsMediaEditActionExtender::TAG
Extensions page status contentExtensionsPageExtender::TAG
Filament panel configurationAdminPanelExtender::TAG
Admin header toolsAdminToolItem::TAG

Prefer extenders over modifying admin resources directly.

Use the abstract schema extenders when possible:

  • AbstractPageSchemaExtender
  • AbstractSiteSchemaExtender
  • AbstractUserSchemaExtender

They provide no-op defaults so a package only overrides the hooks it needs. See Schema Hooks for method signatures, hook enums, and resolver debugging.

The main Pages table renders one core publish/workflow status column. Admin owns the column layout; packages that provide workflow semantics should replace the resolver instead of adding a competing status column.

Bind Capell\Admin\Contracts\Pages\PageTableStatusResolver from the package admin provider:

use Capell\Admin\Contracts\Pages\PageTableStatusResolver;
use Capell\Admin\Data\Pages\PageTableStatusData;
use Capell\Core\Models\Page;
use Filament\Support\Icons\Heroicon;
use Illuminate\Database\Eloquent\Builder;
$this->app->singleton(PageTableStatusResolver::class, ExampleWorkflowPageStatusResolver::class);
final class ExampleWorkflowPageStatusResolver implements PageTableStatusResolver
{
/**
* @param Builder<Page> $query
* @return Builder<Page>
*/
public function modifyQuery(Builder $query): Builder
{
return $query->with('latestWorkflowStep');
}
public function resolve(Page $page): PageTableStatusData
{
return new PageTableStatusData(
label: __('capell-example::workflow.awaiting_review'),
shortLabel: __('capell-example::workflow.review_short'),
tooltip: __('capell-example::workflow.awaiting_review_tooltip'),
color: 'info',
icon: Heroicon::OutlinedClipboardDocumentCheck,
);
}
}

Core falls back to publish-date states: deleted, expired, scheduled, and published. Approval, draft, rollback, and workspace states belong in workflow packages through this resolver.

/admin/extensions is the Extensions dashboard. Keep package management pages in the Filament left sub-navigation by registering them with CapellAdmin::registerExtensionPage($packageName, PageClass::class).

Use dashboard widgets for operational package status, health checks, shortcuts, and marketplace-style actions that belong on the overview. Register widgets against the Extensions dashboard scope:

use Capell\Admin\Enums\DashboardEnum;
use Capell\Admin\Facades\CapellAdmin;
CapellAdmin::registerDashboardWidget(
ExampleExtensionHealthWidget::class,
DashboardEnum::Extensions,
);

Admin bridges can use the convenience method:

$registrar->extensionDashboardWidget(ExampleExtensionHealthWidget::class);

Every extension dashboard widget must use a globally unique settingsKey(), preferably package-prefixed, so dashboard customisation can enable, disable, reorder, and resize it without colliding with core widgets.

Extension dashboard widgets may implement Capell\Admin\Contracts\Extensions\ExtensionDashboardWidgetContract when they want to expose their package-author metadata explicitly. The contract defines the widget settings key, label, description, default span, default order, dashboard scope, and canView() gate. Existing CapellWidgetContract widgets remain supported; the contract is for packages that want their dashboard contribution to be self-describing.

Packages can also contribute operation data without coupling to the Filament UI. Register providers from an admin bridge:

$registrar->extensionHealthProvider(ExampleHealthProvider::class);
$registrar->extensionRuntimeCheckProvider(ExampleRuntimeProvider::class);
$registrar->extensionQuickActionProvider(ExampleQuickActionProvider::class);
$registrar->extensionUpdateMetadataProvider(ExampleUpdateProvider::class);
$registrar->extensionDependencyProvider(ExampleDependencyProvider::class);

Provider contracts live in Capell\Admin\Contracts\Extensions:

ContractUse
ExtensionHealthProviderAdds package health alerts to the diagnostics surface.
ExtensionRuntimeCheckProviderAdds runtime compatibility checks such as queues, cache stores, services, or package-specific gates.
ExtensionQuickActionProviderAdds safe package-level operational shortcuts. Keep destructive work permission-gated.
ExtensionUpdateMetadataProviderSupplies update readiness states when metadata comes from a package or marketplace integration.
ExtensionDependencyProviderAdds uninstall, disable, or update blockers beyond Capell core package protection.

Core catches health provider failures and records a warning diagnostic for the affected package instead of breaking the dashboard. Marketplace integration remains optional; core widgets work from local manifests, installed package data, extension records, runtime gates, and health alerts.

Use Capell\Admin\Support\Extensions\ExtensionsPageActionRegistry when a package needs to add a command to the Extensions page.

Register header actions for package-level work such as installing example content, syncing metadata, or opening a setup flow. Register table actions only when the action belongs to a specific extension row.

The Extensions page already provides core row actions such as documentation and uninstall where available, so packages should not duplicate those actions.

See How To Create A Capell Extension for the full package authoring flow and code examples.

Optional packages should extend the core Extensions page instead of registering a competing package manager page.

Use ExtensionsPageActionRegistry for header actions that open modal workflows:

resolve(ExtensionsPageActionRegistry::class)->registerHeaderAction(
fn (ExtensionsPage $page): Action => Action::make('examplePackageAction')
->label(__('capell-example::actions.example'))
->modalContent(view('capell-example::filament.extensions.example-modal')),
);

Use ExtensionsPageExtender::TAG for status alerts or explanatory blocks shown in the Extensions dashboard actions area. Keep package-specific business logic inside the package contributor. New operational surfaces should prefer an Extensions dashboard widget.

Marketplace connection UI should stay action-oriented:

  • When the site is not connected, show the connection state and the Connect Capell account action.
  • When the site is connected, do not render a success alert above the table. Show Open Marketplace as the primary header action.
  • Open Marketplace opens the marketplace browser in a Filament modal from /admin/extensions, keeping installed extensions and marketplace browsing in one workflow.
  • Do not use a large success alert for the connected state. The account link is a means to access Marketplace, not the destination.
  • Marketplace catalogue API calls must request JSON and use a sort value the Capell app API supports. The safe default is recommended; Capell app also accepts the browser sort values featured_latest, latest, price_low, price_high, and name.

Use Filament page methods for labels, icons, groups, and sort order. Store labels in package translation files.

If a package uses a custom parent navigation group from getNavigationGroup(), register that group from the package admin provider:

use Capell\Admin\Enums\NavigationGroupPositionEnum;
use Capell\Admin\Facades\CapellAdmin;
use Filament\Support\Icons\Heroicon;
CapellAdmin::registerNavigationGroup(
label: 'capell-example::navigation.example',
icon: Heroicon::OutlinedSquares2X2,
position: NavigationGroupPositionEnum::After,
relativeTo: 'capell-admin::navigation.group_content',
);

Capell resolves translated labels before merging, so multiple packages can register the same group independently without creating duplicate sidebar groups. Position can be Start, End, Before, or After; Before and After require relativeTo.

If an admin contribution is missing, start with Extension Troubleshooting. The common fixes are:

  • confirm the package admin provider is loaded;
  • confirm the bridge was registered and booted for the package name;
  • run php artisan capell:admin-clear-cache;
  • run php artisan capell:admin-cache-configurators for configurators and schema extenders;
  • run php artisan capell:admin-cache-widgets for widgets;
  • re-run php artisan capell:admin-install when permissions or policies changed.