
August 12, 2026
9 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Shipping unfinished code to production is inevitable, but exposing it to every user simultaneously is a choice you can avoid. A proper Laravel Pennant feature flags implementation gives you granular control over who sees new functionality, allowing you to test changes on internal staff or specific beta users before a general release. If you are building complex systems like the custom admin panels or legal-tech portals I frequently develop, decoupling deployment from release is essential for maintaining stability while iterating quickly.
Pennant::active() facade or Blade directives. It enables safe, targeted rollouts without redeploying code.How do you install and configure Laravel Pennant feature flags?
Before starting your Laravel Pennant feature flags implementation, verify your environment meets current 2026 requirements: PHP 8.2 or higher and Laravel 11.x or 12.x. Pennant is a first-party package but is not included in the default skeleton, so you must require it explicitly.
composer require laravel/pennantAfter installation, publish the migration and run it. This creates the features table which stores the state of every flag for every user or scope you target.
php artisan vendor:publish --provider="Laravel\Pennant\PennantServiceProvider" --tag="pennant-migrations" php artisan migrateThe published configuration file at config/pennant.php defines your default driver. For most production applications, use the database driver. The array driver is useful only for testing or local development where persistence across requests is unnecessary. In my experience working on production Laravel applications for Nepal-based clients, sticking to the database driver prevents confusion when multiple team members or CI runners interact with the same environment.
Defining features in the service provider
Register your features in the boot method of AppServiceProvider. This acts as your single source of truth. Defining features here ensures they appear in the admin UI (if you build one) and have consistent default values.
use Laravel\Pennant\Feature; use App\Models\User; public function boot(): void { Feature::define('new-client-portal', function (User $user) { // Default logic: enable for admins and beta testers return $user->hasRole('admin') || $user->is_beta_tester; }); Feature::define('dark-mode', true); // Global on/off }A common mistake during Laravel Pennant feature flags implementation is forgetting that closure-based definitions serve as the default resolution. If you later store an explicit value in the database via Feature::activate('new-client-portal'), that stored value overrides the closure entirely. This precedence model is what makes Pennant powerful for both automated eligibility and manual overrides.
How does Pennant resolve feature state for different users?
Understanding scope resolution is critical for any Laravel Pennant feature flags implementation. Pennant checks features against a "scope," which defaults to the currently authenticated user. When you call Feature::active('new-client-portal'), Pennant queries the database for a record matching the feature name and the current user's type and ID.
- No stored record exists: Pennant executes the closure defined in the service provider.
- A stored record exists: Pennant returns the stored boolean value immediately, skipping the closure.
- Null scope: If no user is logged in and you check a user-scoped feature, Pennant returns
falseunless you define a global fallback.
For non-user-specific features like maintenance mode or API version toggles, pass null explicitly or use a dedicated system scope. On a recent legal-tech portal project, we used a string scope 'system' for global configuration flags to avoid accidental coupling with user authentication state.
Targeting specific user segments
You can activate or deactivate features for individual users, roles, or custom scopes programmatically. This is typically done via an admin interface, Artisan command, or seeder.
// Activate for a specific user Feature::for($user)->activate('new-client-portal'); // Deactivate for all users in a role $admins = User::where('role', 'admin')->get(); Feature::for($admins)->deactivate('new-client-portal'); // Set a global override (null scope) Feature::for(null)->activate('maintenance-mode');When implementing this in a Filament admin panel, create a dedicated resource for feature management. This allows non-technical stakeholders to toggle beta features without touching code or SSH access, which is particularly valuable for client-managed projects.
What are the practical patterns for using Pennant in Blade and controllers?
The real value of Laravel Pennant feature flags implementation emerges in how cleanly it integrates into your application layer. Avoid raw if statements scattered throughout business logic; use Pennant's built-in helpers to keep intent clear.
Blade directives for view-layer toggles
Pennant provides @feature and @featurenot directives that compile to efficient PHP checks. These are preferable to passing variables from every controller.
@feature('new-client-portal') <x-portal-beta-banner /> @include('portal.v2.dashboard') @else @include('portal.v1.dashboard') @endfeature @featurenot('dark-mode') <link rel="stylesheet" href="{{ asset('css/light-theme.css') }}"> @endfeaturenotIn controllers, use the facade or the when helper for conditional logic. The when method is especially useful for avoiding nested conditionals in complex workflows like checkout or document processing.
use Laravel\Pennant\Feature; public function showDashboard() { $view = Feature::active('new-client-portal') ? 'portal.v2.dashboard' : 'portal.v1.dashboard'; return view($view); } // Or using the fluent when() helper Feature::when('new-billing-api', fn () => $this->processBillingV2(), fn () => $this->processBillingLegacy() );Eager loading to prevent N+1 queries
A frequent performance pitfall in Laravel Pennant feature flags implementation is checking multiple features per request without eager loading. Each Feature::active() call triggers a separate database query if the result isn't cached. Load all needed features upfront in middleware or a view composer.
// In middleware or controller Feature::load(['new-client-portal', 'dark-mode', 'beta-search']); // Now subsequent checks hit memory, not DB if (Feature::active('new-client-portal')) { ... } if (Feature::active('dark-mode')) { ... }On high-traffic eCommerce sites I've maintained, failing to eager load added 5–15ms per request due to repeated database round-trips. Always profile your feature checks in production-like environments.
How do Pennant feature flags compare to environment variables and config toggles?
Many developers initially reach for .env variables or config files for feature toggling. Understanding why this fails at scale clarifies the value of a dedicated Laravel Pennant feature flags implementation.
| Criteria | Environment Variables | Config Files | Laravel Pennant |
|---|---|---|---|
| Per-user targeting | Impossible | Impossible | Native support |
| Runtime changes | Requires restart/cache clear | Requires cache clear | Instant, no deploy |
| Audit trail | None | Git history only | Database records + timestamps |
| Gradual rollout | All-or-nothing | All-or-nothing | User-by-user or segment |
| Non-dev access | SSH/server access required | Code deployment required | Admin panel friendly |
| Testing in production | Risky, affects all users | Risky, affects all users | Safe, scoped to testers |
Environment variables suit binary infrastructure decisions (mail driver, queue connection). They fail completely for product features that need gradual exposure or user-specific behavior. Config files share the same limitation plus the operational overhead of cache invalidation after every change. Pennant solves both problems by treating features as first-class, persistent, scoped data.
What are the production safety considerations for Laravel Pennant?
A mature Laravel Pennant feature flags implementation requires discipline beyond installation. Features left dormant become technical debt; misconfigured scopes cause silent failures; unchecked growth degrades performance.
Cleanup and lifecycle management
Every feature flag should have an expiration plan. Once a feature reaches 100% rollout and has been stable for a reasonable period (typically 2–4 weeks), remove the flag entirely. This means deleting the definition, removing conditional branches, and purging database records.
// Purge old feature records after full rollout php artisan pennant:purge --feature=new-client-portal // Or purge all inactive features older than 90 days // (Custom command recommended for this)I maintain a simple spreadsheet or Notion tracker listing active flags, owners, and target removal dates. Without this, teams accumulate dozens of dead flags that confuse new developers and slow down queries.
Testing strategy
Pennant includes testing helpers that prevent false positives in your test suite. Always fake features in tests rather than relying on database state.
use Laravel\Pennant\Feature; test('beta users see new portal', function () { Feature::fake(['new-client-portal' => true]); $response = $this->actingAs($betaUser) ->get('/dashboard'); $response->assertSee('Welcome to Beta Portal'); }); test('regular users see legacy portal', function () { Feature::fake(['new-client-portal' => false]); $response = $this->actingAs($regularUser) ->get('/dashboard'); $response->assertSee('Dashboard'); });Never test against live database flags. Faking ensures deterministic results and keeps your test suite fast. For more on structuring testable Laravel applications, see this guide on modern Laravel architecture best practices.
Performance monitoring
Add the features table to your database monitoring. Watch for index bloat as user counts grow. Ensure the composite index on (name, scope_type, scope_id) exists and is used. On applications with hundreds of thousands of users, consider partitioning or archiving old flag states. Redis caching is supported natively and recommended for high-read scenarios where database latency becomes noticeable.
Implementing Laravel Pennant Feature Flags Safely in Production
A disciplined Laravel Pennant feature flags implementation transforms how you ship software. You gain the confidence to deploy incomplete work, test with real users in production, and roll back instantly without code changes. Start with the database driver, define features in your service provider, eager load to avoid N+1 issues, and establish a cleanup cadence from day one. If you need help architecting a safe release workflow for your Laravel application or want to discuss hiring a Laravel developer in Nepal for your next project, reach out directly to discuss your specific requirements.

