Laravel Pennant: Scoped Feature Flags &amp; Custom Drivers | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com) [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles  ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

  [ home ](https://www.msaied.com)    [ articles ](https://www.msaied.com/articles)    Laravel Pennant: Feature Flags with Scope, Storage, and Custom Drivers        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: Feature Flags with Scope, Storage, and Custom Drivers](https://cdn.msaied.com/212/ab98aa676ce445275d736755a046b360.png)

  #laravel   #feature-flags   #pennant   #multi-tenant   #testing  

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

     16 Jun 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Laravel Pennant Beyond the Basics  ](#laravel-pennant-beyond-the-basics)
2. [  02   Scoping Flags to a Tenant  ](#scoping-flags-to-a-tenant)
3. [  03   Choosing the Right Storage Driver  ](#choosing-the-right-storage-driver)
4. [  04   Rich Feature Definitions with Class-Based Resolvers  ](#rich-feature-definitions-with-class-based-resolvers)
5. [  05   Testing Without Leakage  ](#testing-without-leakage)
6. [  06   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.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Flaravel-pennant-feature-flags-with-scope-storage-and-custom-drivers&text=Laravel+Pennant%3A+Feature+Flags+with+Scope%2C+Storage%2C+and+Custom+Drivers) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Flaravel-pennant-feature-flags-with-scope-storage-and-custom-drivers) 

 Frequently Asked Questions 
----------------------------

  3 questions  

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

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

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

  Continue reading

 More Articles 
---------------

 [ View all    ](https://www.msaied.com/articles) 

 [ ![Laravel Telescope Alternatives: Building a Lightweight Request Inspector with Middleware](https://cdn.msaied.com/216/9b6d240010b80483f072902dafcd216c.png) laravel middleware debugging 

### Laravel Telescope Alternatives: Building a Lightweight Request Inspector with Middleware

Telescope is powerful but heavy for production. Learn how to build a focused, low-overhead request inspector u...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 16 Jun 2026     1 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-telescope-alternatives-building-a-lightweight-request-inspector-with-middleware) [ ![RAG Pipelines in Laravel: Chunking, Embedding, and Retrieval with pgvector](https://cdn.msaied.com/215/e037e13535aa77822f879ee829ec3f68.png) laravel ai pgvector 

### RAG Pipelines in Laravel: Chunking, Embedding, and Retrieval with pgvector

Build a production-ready Retrieval-Augmented Generation pipeline in Laravel using pgvector, OpenAI embeddings,...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 16 Jun 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/rag-pipelines-in-laravel-chunking-embedding-and-retrieval-with-pgvector) [ ![Laravel Pest: Architecture Tests, Mutation Testing, and Type Coverage in CI](https://cdn.msaied.com/214/0d4822fa8ee1765d0689e387dd849d92.png) laravel pest testing 

### Laravel Pest: Architecture Tests, Mutation Testing, and Type Coverage in CI

Go beyond feature tests. Learn how to enforce architectural rules, catch logic gaps with mutation testing, and...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 16 Jun 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-pest-architecture-tests-mutation-testing-and-type-coverage-in-ci) 

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

Explore

- [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
