ryangjchandler/filament-feature-flags
Filament Feature Flags adds a simple feature flag system to Filament panels, letting you toggle features on and off per user or globally using Laravel's Pennant. Define flags, control access with policies, and conditionally show resources, pages, and widgets.
This plugin is a Filament panel page that reads and writes Laravel Pennant feature flags, so you need both pieces before anything shows up.
composer require laravel/pennant
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
php artisan migrate
composer require ryangjchandler/filament-feature-flags
php artisan vendor:publish --tag="filament-feature-flags-config"
use RyanChandler\FilamentFeatureFlags\FeatureFlagsPlugin;
public function panel(Panel $panel): Panel
{
return $panel->plugin(FeatureFlagsPlugin::make());
}
Feature::active('my-feature'); // true
Define features in one place, reference them by stable string key. The key you put in config is the Pennant key written to the features table. Treat it like a database column name — never rename it casually, or you orphan activation rows and silently reset behaviour.
Separate "global" flags from "scoped" flags. Pennant's default scope is the authenticated user, but most real deployments need one of:
Feature::for($user)->active('beta-dashboard'); expose the User (and Team, if you're multi-tenant) as a scope in the plugin so admins can toggle individuals.Feature::for(tenant())->active(...); don't rely on the session-resolved user inside tenant context.Guard at the edges, not deep in the domain. The cleanest pattern is a middleware alias or a Blade directive that reads the flag once and either aborts or swaps a component:
// app/Http/Middleware/EnsureFeatureActive.php
if (! Feature::active($feature)) {
abort(404);
}
Then ->middleware('feature:billing-v2') on the route group. Keeping checks at the boundary means deleting the flag later is a one-line change per route.
Put defaults in code, overrides in the UI. Set the intended default (true/false) in config or a seeder so a fresh environment behaves predictably, and let the admin panel only ever express deliberate overrides. This is what saves you when someone wipes the features table in staging.
Scope-aware seeding for demos and tests. Rather than clicking the UI, drive state programmatically:
Feature::for($user)->activate('new-checkout');
Feature::for($user)->deactivate('new-checkout');
Feature::forget(); // drop Pennant's in-request cache
Wire it into your deploy checklist. Because the panel writes to the database, feature state is data, not config. It does not travel with your code, so staging→production promotion needs an explicit decision about which flags carry over.
Pennant caches resolved values in memory for the request. If you toggle a flag mid-request (or in a test right after the app has already resolved it), you'll get the stale value. Call Feature::forget() — or Feature::purge() for a full reset — after any programmatic change. This is the single most common source of "the button doesn't work" confusion.
Nothing renders without the migrations. If the panel loads but toggles don't persist, or the page 500s, check that Pennant's features table exists in that connection. In multi-database setups the plugin will happily write to the wrong connection.
An undefined feature resolves to false, not an error. Feature::active('typo-here') fails silently. Cross-check config keys against Feature::active(...) calls in your codebase — a rename on one side and not the other is invisible until a user complains.
Config caching in production. If feature definitions live in the plugin config, php artisan config:cache during deploy will freeze them until the next cache clear. Make sure your deploy script runs it, and remember that any config change requires a re-cache.
Lock down the panel page. The plugin registers a Filament page; it inherits whatever your panel's access gate is. If your admin panel is reachable by more than a handful of people, add an explicit canAccess() / policy check — feature toggles are effectively production switches and shouldn't be available to every panel user by default.
Zero dependents and no star history means you are the QA team. Pin the version in composer.json, read the source once (it's small), and don't upgrade casually across Filament majors. Filament v3→v4 plugin APIs and page registration have both moved before.
Have an escape hatch. If the package stalls, the UI is thin: you can replace it with a custom Filament page over Pennant in an afternoon. Keep your feature names in config/constants rather than hardcoded strings in views so the swap is mechanical.
Treat flags as debt with an expiry. Add the flag name to the PR that introduces it and open a follow-up to remove the branch once it hits 100%. Panels like this make flags easy to create and easy to forget — the graveyard of permanently-on flags is the real failure mode, not the plugin itself.
How can I help you explore Laravel packages today?