Skip to content

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.

Laravel 12 and 13 PHP 8.3+ v1.0.1 Free · MIT
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
The Pennant Dashboard page for the new-checkout feature, marked closure, rollout-ready and 25% rollout with 53 on and 149 off: Activate and Deactivate for everyone buttons, a percentage slider set to 25, a one-scope form with Activate, Deactivate, Forget and Check value, and a table of stored values per user showing one override and the rest set by rollout bucket.
The Pennant Dashboard page for the new-checkout feature, marked closure, rollout-ready and 25% rollout with 53 on and 149 off: Activate and Deactivate for everyone buttons, a percentage slider set to 25, a one-scope form with Activate, Deactivate, Forget and Check value, and a table of stored values per user showing one override and the rest set by rollout bucket.

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. 1 composer require amjadiqbal/laravel-pennant-dashboard
  2. 2 php artisan migrate
  3. 3 Define the viewPennantDashboard gate (and optionally managePennantDashboard).
  4. 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 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 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 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 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.

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.

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.

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.

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 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 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 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 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.

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.

Need something built?

Free quote in 60 seconds