Laravel Pennant: How I Add Feature Flags Without a Third-Party Service
There's a moment in almost every SaaS project where you need to ship something to just a handful of users first. Maybe it's a redesigned billing flow, a new onboarding sequence, or a half-finished dashboard you want early adopters to test. You want to deploy the code — but not show it to everyone yet.
For years, my go-to answer was a rough if ($user->isBeta()) check buried somewhere in a controller. Effective? Barely. Maintainable? Absolutely not. Then I started reaching for third-party feature flag services, which introduced another subscription, another API dependency, and another thing to go wrong at 2am.
Then Laravel Pennant arrived, and I haven't looked back.
What Pennant Actually Is
Laravel Pennant is an official first-party package for managing feature flags. It ships with two storage drivers out of the box — database and array — and slots cleanly into any Laravel 10+ project (and obviously Laravel 12, which is what I'm building on now).
It's not trying to be LaunchDarkly. It's not a full A/B testing platform. What it is is a clean, expressive way to define whether a feature is active for a given user, team, or any other model — and to check that consistently across your entire application.
Installing and Getting Started
Installation is as simple as it gets:
composer require laravel/pennant
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider"
php artisan migrate
That gives you a features table in your database and a config/pennant.php file to work with.
Defining Features
Features are defined in a AppServiceProvider or a dedicated service provider. I prefer a dedicated one — keeps things tidy:
use Laravel\Pennant\Feature;
Feature::define('new-billing-flow', function (User $user) {
return $user->created_at->isAfter(now()->subDays(30));
});
This feature is active for any user who signed up in the last 30 days. Clean, readable, and entirely in PHP — no dashboard to log into, no JSON config files.
You can also use class-based features for anything more complex:
php artisan pennant:feature NewBillingFlow
This generates a feature class with a resolve method, which I find much nicer for anything beyond a one-liner.
Checking Features Everywhere
This is where Pennant really shines. You can check features in controllers, Livewire components, Blade views — anywhere.
In a controller:
if (Feature::active('new-billing-flow')) {
return redirect()->route('billing.new');
}
In a Livewire component:
use Laravel\Pennant\Feature;
public function mount()
{
$this->showNewFlow = Feature::for(auth()->user())->active('new-billing-flow');
}
In Blade:
@feature('new-billing-flow')
<x-new-billing-form />
@else
<x-old-billing-form />
@endfeature
That Blade directive alone made me happy. It's so much cleaner than @if (Feature::active(...)) every time.
Scope: The Bit Most Tutorials Skip
By default, Pennant scopes features to the authenticated user. But you're not limited to that. In a multi-tenant SaaS, you might want features scoped to a team or organisation instead.
Feature::for($team)->active('advanced-reporting');
This is genuinely powerful. You can roll out features per subscription tier, per team, per plan — whatever makes sense for your product. I use this constantly to gate functionality behind higher-tier plans without touching billing logic in my feature definitions.
Eagerly Loading Features to Avoid N+1 Problems
One thing to watch: if you're checking features in a loop — say, rendering a list of users in an admin panel — you can run into N+1 database queries fast. Pennant handles this with eager loading:
Feature::for($users)->load(['new-billing-flow', 'advanced-reporting']);
Call this before your loop and Pennant will batch the queries. Same pattern you'd use with Eloquent relationships. Once you know it's there, you'll reach for it automatically.
Lottery-Based Rollouts
Pennant has a lovely little helper I didn't expect: Lottery. This lets you roll features out to a random percentage of users:
use Illuminate\Support\Lottery;
Feature::define('new-dashboard', Lottery::odds(1, 10));
That activates the feature for roughly 10% of users, chosen at random. Combined with the database driver, the result is stored so the same user always gets the same experience. No flickering, no inconsistency.
I used this recently to test a redesigned onboarding flow on a small slice of new signups before rolling it out fully. Worked perfectly.
Retiring Features the Right Way
Here's the discipline part that most developers skip: cleaning up old flags. Pennant makes this approachable with the purge method:
Feature::purge('old-onboarding-flow');
This removes all stored values for that feature from the database. Once you've fully shipped something and removed the flag from your code, run this and you're clean. I add it to my deployment checklist.
My Honest Take
Pennant isn't trying to replace enterprise feature management platforms. If you need complex targeting rules, analytics, multi-environment syncing across a team of fifty, go look at something more specialised.
But for the vast majority of Laravel SaaS projects — and honestly, for everything I build — Pennant is exactly the right tool. It's in the framework, it's well-maintained, it performs well, and it encourages you to write clean, explicit code rather than scattering conditionals everywhere.
I've removed two third-party subscriptions from projects this year by switching to Pennant. That's fewer moving parts, lower cost, and less to explain to clients.
If you haven't tried it yet, give it an afternoon. I think you'll find it earns a permanent place in your stack.