Laravel Pennant: Scoped Feature Flags &amp; Custom Drivers | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Laravel Pennant: Feature Flags with Scope, Storage, and Custom Drivers

 Laravel Pennant: Feature Flags with Scope, Storage, and Custom Drivers
=======================================================================

 Laravel Pennant ships with more power than most teams use. Learn how to scope flags to tenants, swap storage drivers, and write custom resolvers that keep your feature rollout logic clean and testable.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 16 Jun 2026 · Updated 16 Jun 2026 · 4 min read

ShareCopy linkCopied

 ![Laravel Pennant: Feature Flags with Scope, Storage, and Custom Drivers](https://cdn.msaied.com/212/ab98aa676ce445275d736755a046b360.png) 

  On this page +1. [Laravel Pennant Beyond the Basics](#laravel-pennant-beyond-the-basics)
2. [Scoping Flags to a Tenant](#scoping-flags-to-a-tenant)
3. [Choosing the Right Storage Driver](#choosing-the-right-storage-driver)
4. [Rich Feature Definitions with Class-Based Resolvers](#rich-feature-definitions-with-class-based-resolvers)
5. [Testing Without Leakage](#testing-without-leakage)
6. [Key Takeaways](#key-takeaways)

 Laravel Pennant Beyond the Basics
---------------------------------

Most teams discover Pennant, add a few `Feature::define()` calls, and ship. That works fine until you need per-tenant flags, a custom storage backend, or a rollout strategy that isn't a simple percentage. This article covers the parts of Pennant that rarely appear in introductory posts.

---

### Scoping Flags to a Tenant

Pennant resolves features against a *scope* — by default the authenticated user. In a multi-tenant app you usually want the **team or organisation** as the scope, not the individual.

```php
// AppServiceProvider::boot()
Feature::resolveScopeUsing(fn ($driver) => request()->user()?->currentTeam);

```

Now every `Feature::active('billing-v2')` call is automatically scoped to the current team. You can also pass an explicit scope anywhere:

```php
Feature::for($team)->active('billing-v2');

```

This is critical when a job runs outside an HTTP request. Inject the team and pass it explicitly rather than relying on the global resolver:

```php
class ActivateBillingJob implements ShouldQueue
{
    public function __construct(private Team $team) {}

    public function handle(): void
    {
        if (Feature::for($this->team)->active('billing-v2')) {
            // ...
        }
    }
}

```

---

### Choosing the Right Storage Driver

Pennant ships with two drivers: `database` and `array`. The `array` driver is perfect for tests — it never persists. The `database` driver stores one row per (feature, scope) pair.

For high-traffic apps, that table can become a hot spot. A common fix is to front the database driver with a cache layer using a **custom driver**.

```php
// PennantServiceProvider::boot()
Feature::extend('cached-db', function (Application $app) {
    return new CachedDatabaseDriver(
        $app->make(DatabaseDriver::class),
        $app->make(Repository::class), // Cache
        ttl: 300
    );
});

```

```php
class CachedDatabaseDriver implements Driver
{
    public function __construct(
        private Driver $db,
        private Repository $cache,
        private int $ttl,
    ) {}

    public function get(string $feature, mixed $scope): mixed
    {
        $key = "pennant:{$feature}:" . $this->scopeKey($scope);

        return $this->cache->remember($key, $this->ttl,
            fn () => $this->db->get($feature, $scope)
        );
    }

    public function set(string $feature, mixed $scope, mixed $value): void
    {
        $this->db->set($feature, $scope, $value);
        $this->cache->forget("pennant:{$feature}:" . $this->scopeKey($scope));
    }

    private function scopeKey(mixed $scope): string
    {
        return $scope instanceof Model ? $scope->getKey() : (string) $scope;
    }

    // Implement define(), getAll(), delete(), purge() by delegating to $this->db
}

```

Set `PENNANT_STORE=cached-db` in your env and register the driver name in `config/pennant.php`.

---

### Rich Feature Definitions with Class-Based Resolvers

Inline closures in `Feature::define()` get messy. Extract logic into invokable classes and bind them through the container:

```php
Feature::define('new-checkout', NewCheckoutFeature::class);

```

```php
class NewCheckoutFeature
{
    public function __construct(private RolloutService $rollout) {}

    public function __invoke(Team $team): bool
    {
        return $this->rollout->teamIsInCohort($team, 'checkout-beta');
    }
}

```

Pennant resolves the class through the service container, so constructor injection works out of the box. This makes the resolver independently unit-testable:

```php
it('activates new checkout for beta cohort teams', function () {
    $rollout = Mockery::mock(RolloutService::class);
    $rollout->shouldReceive('teamIsInCohort')->andReturn(true);

    $feature = new NewCheckoutFeature($rollout);

    expect($feature($this->team))->toBeTrue();
});

```

---

### Testing Without Leakage

Always use the `array` driver in tests to avoid database hits and cross-test contamination:

```php
// phpunit.xml or Pest's beforeEach
config(['pennant.default' => 'array']);

```

Or activate/deactivate inline:

```php
Feature::activate('billing-v2');
Feature::deactivate('billing-v2');

```

Pennant resets the `array` store between requests automatically, but in long-running Octane workers you must call `Feature::flushCache()` at the start of each request — add it to a middleware.

---

### Key Takeaways

- Scope flags to teams, not users, in multi-tenant apps — use `resolveScopeUsing` globally and `Feature::for()` in jobs.
- The `database` driver can become a bottleneck; wrap it in a custom cached driver for read-heavy workloads.
- Class-based resolvers are container-resolved, making them trivially testable in isolation.
- Always run the `array` driver in tests and flush the cache in Octane middleware to prevent state leakage.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [feature-flags](https://www.msaied.com/public/articles?search=feature-flags)
- [pennant](https://www.msaied.com/public/articles?search=pennant)
- [multi-tenant](https://www.msaied.com/public/articles?search=multi-tenant)
- [testing](https://www.msaied.com/public/articles?search=testing)

 Frequently asked questions 
---------------------------

  Can Pennant scope feature flags to a team instead of the authenticated user?Yes. Call `Feature::resolveScopeUsing()` in a service provider to return your team model as the default scope, or pass an explicit scope with `Feature::for($team)-&gt;active('flag-name')` anywhere in your codebase.

   How do I prevent Pennant from hitting the database on every request?Implement a custom driver that wraps the built-in `DatabaseDriver` with a cache layer. Register it via `Feature::extend()` and set it as the default store. Remember to invalidate the cache key in the `set()` method.

   How should I handle Pennant in Laravel Octane workers?Pennant's in-memory cache persists across requests in long-running workers. Add a middleware that calls `Feature::flushCache()` at the start of each request to prevent stale flag values leaking between users.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleLaravel Eloquent Global Scopes: Pitfalls, Testing, and Composing Them Safely](https://www.msaied.com/public/articles/laravel-eloquent-global-scopes-pitfalls-testing-and-composing-them-safely) [Next articleLaravel Pest: Architecture Tests, Mutation Testing, and Type Coverage in CI](https://www.msaied.com/public/articles/laravel-pest-architecture-tests-mutation-testing-and-type-coverage-in-ci)  

   On this page
-------------

1. [Laravel Pennant Beyond the Basics](#laravel-pennant-beyond-the-basics)
2. [Scoping Flags to a Tenant](#scoping-flags-to-a-tenant)
3. [Choosing the Right Storage Driver](#choosing-the-right-storage-driver)
4. [Rich Feature Definitions with Class-Based Resolvers](#rich-feature-definitions-with-class-based-resolvers)
5. [Testing Without Leakage](#testing-without-leakage)
6. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
