Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Pennant Feature Flags Implementation

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.

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/pennant

After 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 migrate

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

Composer Requirelaravel/pennantPublish & Migratefeatures tableConfig Driverdatabase / arrayReadyUse Facade
Standard Laravel Pennant feature flags implementation setup sequence from installation to active usage

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

Check FeatureDB Record Exists?YESNOReturn Stored Value(Skip Closure)Execute Closure(Default Logic)Active / InactiveActive / Inactive
Resolution order: stored database values always take precedence over closure-based defaults in Pennant

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') }}"> @endfeaturenot

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

CriteriaEnvironment VariablesConfig FilesLaravel Pennant
Per-user targetingImpossibleImpossibleNative support
Runtime changesRequires restart/cache clearRequires cache clearInstant, no deploy
Audit trailNoneGit history onlyDatabase records + timestamps
Gradual rolloutAll-or-nothingAll-or-nothingUser-by-user or segment
Non-dev accessSSH/server access requiredCode deployment requiredAdmin panel friendly
Testing in productionRisky, affects all usersRisky, affects all usersSafe, 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.

Env Variables✗ Per-user✗ Runtime✗ Audit✗ Gradual✗ Non-devInfrastructure onlyConfig Files✗ Per-user△ Cache clear△ Git only✗ Gradual✗ Non-devDeploy-boundLaravel Pennant✓ Per-user✓ Instant✓ DB audit✓ Gradual✓ Admin UIProduction-ready
Why dedicated feature flag systems outperform environment variables and config files for product releases

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.

Frequently Asked Questions

Laravel Pennant is a first-party feature flag package providing database-backed, user-specific toggles with an expressive API. Unlike static config flags, Pennant allows runtime changes without redeployment, supports A/B testing per user, and integrates natively with Laravel's authorization and caching systems for production-grade feature management.

Run composer require laravel/pennant on PHP 8.2 or higher. Publish the migration with php artisan vendor:publish --tag=pennant-migrations, then run php artisan migrate. The package auto-discovers its service provider. For Laravel 12, ensure your app uses the default cache driver or configure a dedicated Pennant cache store for optimal performance in high-traffic environments.

Yes, Pennant ships with a database driver using a features table. It stores flag state per scope (user, team, guest). You can also use array or custom drivers. In my experience deploying legal-tech portals, the database driver provides necessary auditability and persistence across deploys when using zero-downtime symlinked releases via Deployer 7.

Yes, use the @feature('flag-name') directive or Feature::active('flag-name') in views. Pennant caches results per request, so repeated checks incur no additional database queries. Always pass explicit scopes like auth()->user() rather than relying on implicit resolution in shared partials to avoid accidental global leakage in multi-tenant applications.

Pennant caches flag states using Laravel's cache system, keyed by flag name and scope. On production Laravel apps I maintain, Redis 7.x handles this efficiently. Cache invalidation happens automatically when flags are updated via the API. For read-heavy workloads, pre-warm cache during deployment or use tagged caching to batch-clear related flags during maintenance windows.

Negligible when cached. Uncached database lookups add 1-3ms per check; cached checks are sub-millisecond via Redis. Pennant batches multiple flag checks into single queries when possible. In real-world eCommerce systems handling thousands of daily visitors, I have never observed measurable latency impact from properly cached Pennant usage, even with dozens of active flags.

Pass the scope as the second argument: Feature::for($user)->active('new-checkout'). Pennant supports User models, teams, guests via session ID, or any identifiable entity. Define default scope behavior in your Feature definition class. For legal-tech client portals, I commonly scope flags to firm IDs or user roles to control document workflow visibility without code changes.

Not natively. Pennant lacks a built-in admin UI. Build a simple controller using Feature::activate() and Feature::deactivate() behind proper authorization policies. On client projects, I create restricted admin panels allowing stakeholders to manage flags safely. Alternatively, use Filament or Nova resources wrapping Pennant's API for rapid internal tooling without exposing raw database access.

Use Feature::fake() in tests to force specific flag states without database interaction. Call Feature::fake(['new-billing' => true]) before assertions. This isolates business logic from flag infrastructure. Always test both active and inactive states. In my experience, untested inactive paths cause more production incidents than the flagged features themselves, especially after flags are eventually removed.

Remove them deliberately. Delete the flag definition, strip conditional checks, and clean database records via php artisan pennant:purge. Leaving dead flags accumulates technical debt and confuses future developers. I schedule quarterly flag audits on maintained projects. Document removal in changelogs. Never let flags persist indefinitely; they become undocumented configuration that breaks the principle of explicit code behavior.

Partially. Pennant supports percentage-based rollouts and consistent user bucketing via its lottery mechanism. However, it lacks statistical analysis, experiment lifecycle management, and result tracking. For simple gradual rollouts, Pennant suffices. For rigorous A/B testing with conversion metrics, integrate with dedicated platforms like PostHog or GrowthBook while using Pennant as the enforcement layer.

Security depends entirely on your implementation. Pennant provides no built-in access control. Always wrap flag mutation endpoints in Laravel Policies or middleware verifying admin privileges. Log all flag changes with user identity and timestamp. In legal-tech systems handling sensitive documents, I enforce strict RBAC via Spatie Permission before allowing any flag modification to prevent accidental exposure of restricted workflows.

Yes, define flags conditionally based on environment variables within your Feature definition classes. Check app()->environment() or config values at definition time. This allows staging-only features or region-specific toggles without separate deployments. Store environment distinctions in .env files managed through your Deployer 7 shared directory configuration, keeping flag logic consistent across release symlinks.

Pennant is self-hosted, free, and tightly integrated with Laravel. Third-party services offer analytics, audit logs, targeting rules, and SDKs across platforms but cost USD 500+ monthly (~NPR 65,000). For Nepal-based clients with budget constraints, Pennant eliminates recurring fees and data residency concerns. Choose external services only when you need cross-platform consistency or advanced experimentation beyond Laravel's ecosystem.

Checking flags without explicit scopes causes unexpected global activation. Forgetting to cache leads to N+1 query patterns in loops. Missing fake() calls make tests flaky. Neglecting cleanup creates zombie flags. Hardcoding flag names instead of using constants causes typo-related bugs. Always define flags in dedicated classes, use typed references, implement comprehensive test coverage, and establish removal procedures before shipping to production.

Share this article

Quick Contact Options
Choose how you want to connect me: