Packstub

Configuration

Key Default Purpose
scope_model App\Models\Team the tenant model (admin scope picker, features:list)
features [] feature definitions (label, type boolean/limit, default, description, unit, group)
plans [] plan definitions (label, description, price, features)
default_plan free plan when the resolver returns nothing
overrides_table feature_overrides
connection null central connection in database-per-tenant apps
cache.store / cache.ttl null / 300 cross-request override cache
upgrade_url null where locked features send people

The plugin API

FeaturesPlugin::make()
    ->features([...])
    ->plans([...], default: 'free')
    ->planUsing(fn (?Model $scope) => ...)      // string, Plan or null
    ->usage('projects', fn (?Model $scope) => ...)
    ->upgradeUrl(fn (?Model $scope) => ...)     // or a string
    ->planPage(MyPlanPage::class)               // null to skip
    ->adminPages()                              // FeatureFlags + FeatureOverrides (central/admin panel)
    ->canManageUsing(fn (User $user) => ...)    // who may open them; see installation
    ->navigationGroup('Account')->navigationSort(90)
    ->config(['cache.store' => 'redis']);

Several panels

Every panel keeps its own FeaturesPlugin definitions. Filament registers all panels at boot, so console commands and queued jobs see the panel registered last; when a panel is served, its features, plans, usage counters and config overrides are applied again for that request. One app can therefore give an admin panel and a customer panel different scope models and plans. Keys a panel leaves unset come from the config file, never from another panel. When a seeder or test switches panels with Filament::setCurrentPanel(), call FeaturesPlugin::get()->configure() afterwards so the runtime follows.

Commands

  • features:list {scope?} — features, defaults, plan values and the resolved value for a scope.
  • features:prune — delete expired overrides.

With Filament Teams

TeamsPlugin::make()->seatLimit(fn (Team $team): ?int => Features::limit('seats', $team));

With Filament Tenancy

Set scope_model to the Tenant model and connection to the central connection; overrides use string scope ids, so Tenancy's string keys work as they are.