Feature flags · Open source
Laravel Pennant Dashboard
A browser dashboard for laravel/pennant feature flags: per-scope toggles, sticky percentage rollouts, purge and an audit log.
What it solves
laravel/pennant has no UI.
The existing ones are a Filament plugin and a Nova tool, so an app on neither has nothing but tinker to see who gets a feature, switch it for one user, or roll it out to a share of users.
An unofficial, independent dashboard for laravel/pennant (not made, endorsed or supported by Laravel). It is plain Blade with forms, so it needs no Filament, Nova or Livewire: install it, run the migration, define a gate and open /pennant.
- Lists every feature in a Pennant store: closures, class-based features in app/Features, and stale stored features
- Turn a feature on or off for one user, team, any Eloquent model, Feature::globally() or a raw scope string
- Turn a feature on or off for everyone with Pennant's own activateForEveryone()
- Sticky percentage rollouts: each scope gets a fixed bucket, so raising 25% to 50% only adds scopes
- Audit log of dashboard changes (actor, IP, old and new value) and of changes made by your code or artisan
- Separate gates for viewing and changing, local environment only by default, plus a read-only switch
Install
Requires PHP 8.3+ and Laravel 12 and 13.
composer require amjadiqbal/laravel-pennant-dashboard
Usage
use AmjadIqbal\PennantDashboard\Rollout;
use Laravel\Pennant\Feature;
// Let the dashboard decide for scopes that have not been checked yet:
// an override for this scope, else the rollout percentage, else your default.
Feature::define('new-checkout', fn (User $user) => Rollout::decide('new-checkout', $user, default: false));
Setup
This is an independent package, not made by Laravel. It installs laravel/pennant 1.18+ if you do not have it; publish and run Pennant's own migration as the Pennant docs describe. The package migration adds three tables: rollout rules, per-scope overrides and the audit log. Then open /pennant; outside the local environment it returns 403 until you define who may see it. When upgrading from 1.0.0, run php artisan migrate again: a new migration adds a store column and assigns existing rollout rules and overrides to the default store.
- 1 composer require amjadiqbal/laravel-pennant-dashboard
- 2 php artisan migrate
- 3 Define the viewPennantDashboard gate (and optionally managePennantDashboard).
- 4 Open /pennant.
Who can open it
viewPennantDashboard decides who can see the dashboard; without your own definition it allows the local environment only, for everyone who can reach the app there. managePennantDashboard decides who can change things and defaults to whoever may view. Both are checked after the configured middleware (default web). PENNANT_DASHBOARD_READ_ONLY=true blocks every change whatever the gates say (the read-only previews stay available), and PENNANT_DASHBOARD_ENABLED=false removes the routes.
use Illuminate\Support\Facades\Gate;
Gate::define('viewPennantDashboard', fn (User $user) => $user->is_admin);
Gate::define('managePennantDashboard', fn (User $user) => $user->email === '[email protected]');
Rollouts and everyone
Pennant stores a value the first time a scope is checked, and activateForEveryone() only rewrites values that are already stored. Features that call Rollout::decide() (or use the HasRollout trait in a class feature) are marked rollout-ready, and scopes checked later follow the dashboard too. A scope's bucket is crc32(feature|scope) % 100, using Pennant's serialized scope; it is in the rollout when its bucket is below the percentage. Setting a percentage re-buckets stored values on the database driver: raising it only adds scopes, lowering it turns off the scopes above the new percentage. Per-scope overrides are kept, and an override always wins. Rules and overrides belong to the Pennant store they were set in; Rollout::decide() reads the default store unless you pass store:.
use AmjadIqbal\PennantDashboard\Concerns\HasRollout;
class NewCheckout
{
use HasRollout;
public function resolve(User $user): bool
{
return $this->rollout($user, default: false);
}
}
Purge and the audit log
Purge deletes a feature's stored values through Pennant's Feature::purge() and clears its overrides, optionally with its rollout; you can also purge every stale feature. The browser asks you to confirm first. The audit log records dashboard changes with the signed-in user, the IP and the old and new value. Changes from your code or artisan are logged too, marked as such, with what Pennant's events carry: the feature, the scope and the new value (no old value and no IP). Entries older than 180 days (audit.retention_days) are removed by model:prune.
Schedule::command('model:prune', ['--model' => [\AmjadIqbal\PennantDashboard\Models\AuditEntry::class]])->daily();
Drivers and things to know
- Database driver: every stored scope and value is listed, with on/off counts and filters.
- Array driver: defined features are listed and changes work for one request.
- Custom drivers: the dashboard reports which Pennant contracts the driver implements and works through them.
- Check value and "What does one scope see?" are read-only previews: the stored value where there is one, otherwise what your resolver returns now, without storing, caching or firing Pennant events. Lotteries are not drawn. Your resolver does run, so keep it free of side effects.
- Rollouts are for on/off features; features with rich values can be viewed, forgotten and purged.
- Scope search matches the ID and the configured search_columns (name and email by default).
Hire me
Need this built into your app, or something custom?
I wrote Laravel Pennant Dashboard. I can wire it into your Laravel app, extend it for your case, or build the feature around it. Tell me what you need and you'll get a plan and a quote, free.
Support
Found a bug or want a feature? Open an issue on GitHub. For help adding it to your own project, use the quote form above.
More Laravel packages
All packages
Laravel Driver.js
Build Driver.js product tours in PHP and render them with one Blade component.
Laravel Sonner
Sonner toast notifications for Laravel, sent from PHP and shown without React.
Laravel Ninja Keys
A Cmd+K / Ctrl+K command palette for Laravel, with the actions defined in PHP.
Laravel SweetAlert2
SweetAlert2 popups, toasts and confirm dialogs from Laravel controllers, Livewire and Alpine.
Laravel Grid.js
Turn any Eloquent query into a searchable, sortable, paginated Grid.js data table with one fluent PHP call.
Laralink
Work on a Composer package inside the Laravel app that uses it, then switch back to the Packagist release.
Refresher
A drop-in replacement for Laravel's RefreshDatabase that restores a cached snapshot of your test database instead of running migrate:fresh.
Kiln
Artisan commands and an optional signed HTTP route to clear, warm and inspect OPcache when you deploy.
Laravel Media Viewer
Show private PDFs, video and audio from any Laravel disk without making the files public.
Laravel Swiper
Swiper sliders, carousels and image galleries for Laravel as Blade components.
Laravel MySQL Advisor
Read-only health checks for the MySQL or MariaDB database behind your Laravel app, with the SQL and the Laravel migration to fix a problem wherever a fix applies.
Laravel Webhook Console
A dashboard and replay tool for the webhooks spatie/laravel-webhook-client stores in your Laravel app.
Laravel Notification Inbox
Catches every non-mail Laravel notification in local and staging instead of sending it, and shows each one as it would look on the other end.